package eu.faircode.email;

/*
    This file is part of FairEmail.

    FairEmail is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    NetGuard is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with NetGuard.  If not, see <http://www.gnu.org/licenses/>.

    Copyright 2018 by Marcel Bokhorst (M66B)
*/

import android.content.Context;
import android.os.Handler;
import android.util.Log;

import com.sun.mail.imap.IMAPFolder;
import com.sun.mail.imap.IMAPMessage;
import com.sun.mail.imap.IMAPStore;
import com.sun.mail.util.FolderClosedIOException;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import javax.mail.FetchProfile;
import javax.mail.Folder;
import javax.mail.FolderClosedException;
import javax.mail.Message;
import javax.mail.MessageRemovedException;
import javax.mail.Session;
import javax.mail.UIDFolder;
import javax.mail.search.BodyTerm;
import javax.mail.search.FromStringTerm;
import javax.mail.search.OrTerm;
import javax.mail.search.RecipientStringTerm;
import javax.mail.search.SubjectTerm;

import androidx.lifecycle.GenericLifecycleObserver;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleOwner;
import androidx.paging.PagedList;

public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMessageEx> {
    private Context context;
    private long fid;
    private String search;
    private int pageSize;
    private Handler handler;
    private IBoundaryCallbackMessages intf;
    private ExecutorService executor = Executors.newSingleThreadExecutor(Helper.backgroundThreadFactory);

    private IMAPStore istore = null;
    private IMAPFolder ifolder = null;
    private Message[] imessages = null;
    private List<Long> existing = new ArrayList<>();
    private int index;
    private boolean searching = false;
    private int loaded = 0;

    interface IBoundaryCallbackMessages {
        void onLoading();

        void onLoaded();

        void onError(Context context, Throwable ex);
    }

    BoundaryCallbackMessages(Context _context, LifecycleOwner owner, long folder, String search, int pageSize, IBoundaryCallbackMessages intf) {
        this.context = _context;
        this.fid = folder;
        this.search = search;
        this.pageSize = pageSize;
        this.handler = new Handler();
        this.intf = intf;

        owner.getLifecycle().addObserver(new GenericLifecycleObserver() {
            @Override
            public void onStateChanged(LifecycleOwner source, Lifecycle.Event event) {
                if (event == Lifecycle.Event.ON_DESTROY) {
                    executor.submit(new Runnable() {
                        @Override
                        public void run() {
                            Log.i(Helper.TAG, "Boundary close");

                            DB db = DB.getInstance(context);
                            for (long id : existing)
                                db.message().setMessageFound(id, false);
                            db.message().deleteFoundMessages();

                            try {
                                if (istore != null)
                                    istore.close();
                            } catch (Throwable ex) {
                                Log.e(Helper.TAG, "Boundary " + ex + "\n" + Log.getStackTraceString(ex));
                            } finally {
                                context = null;
                                istore = null;
                                ifolder = null;
                                imessages = null;
                                existing.clear();
                            }
                        }
                    });
                }
            }
        });
    }

    boolean isSearching() {
        return searching;
    }

    int getLoaded() {
        return loaded;
    }

    @Override
    public void onZeroItemsLoaded() {
        Log.i(Helper.TAG, "onZeroItemsLoaded");
        load();
    }

    @Override
    public void onItemAtEndLoaded(final TupleMessageEx itemAtEnd) {
        Log.i(Helper.TAG, "onItemAtEndLoaded");
        load();
    }

    private void load() {
        executor.submit(new Runnable() {
            @Override
            public void run() {
                try {
                    searching = true;
                    handler.post(new Runnable() {
                        @Override
                        public void run() {
                            intf.onLoading();
                        }
                    });

                    DB db = DB.getInstance(context);
                    EntityFolder folder = db.folder().getFolder(fid);
                    if (folder.account == null) // outbox
                        return;
                    EntityAccount account = db.account().getAccount(folder.account);

                    if (imessages == null) {
                        Properties props = MessageHelper.getSessionProperties(account.auth_type);
                        props.setProperty("mail.imap.throwsearchexception", "true");
                        Session isession = Session.getInstance(props, null);

                        Log.i(Helper.TAG, "Boundary connecting account=" + account.name);
                        istore = (IMAPStore) isession.getStore("imaps");
                        Helper.connect(context, istore, account);

                        Log.i(Helper.TAG, "Boundary opening folder=" + folder.name);
                        ifolder = (IMAPFolder) istore.getFolder(folder.name);
                        ifolder.open(Folder.READ_WRITE);

                        Log.i(Helper.TAG, "Boundary searching=" + search);
                        if (search == null)
                            imessages = ifolder.getMessages();
                        else
                            imessages = ifolder.search(
                                    new OrTerm(
                                            new OrTerm(
                                                    new FromStringTerm(search),
                                                    new RecipientStringTerm(Message.RecipientType.TO, search)
                                            ),
                                            new OrTerm(
                                                    new SubjectTerm(search),
                                                    new BodyTerm(search)
                                            )
                                    )
                            );
                        Log.i(Helper.TAG, "Boundary found messages=" + imessages.length);

                        index = imessages.length - 1;
                    }

                    int count = 0;
                    while (index >= 0 && count < pageSize) {
                        Log.i(Helper.TAG, "Boundary index=" + index);
                        int from = Math.max(0, index - (pageSize - count) + 1);
                        Message[] isub = Arrays.copyOfRange(imessages, from, index + 1);
                        index -= (pageSize - count);

                        FetchProfile fp = new FetchProfile();
                        fp.add(FetchProfile.Item.ENVELOPE);
                        fp.add(FetchProfile.Item.FLAGS);
                        fp.add(FetchProfile.Item.CONTENT_INFO); // body structure
                        fp.add(UIDFolder.FetchProfileItem.UID);
                        fp.add(IMAPFolder.FetchProfileItem.HEADERS);
                        fp.add(FetchProfile.Item.SIZE);
                        fp.add(IMAPFolder.FetchProfileItem.INTERNALDATE);
                        ifolder.fetch(isub, fp);

                        try {
                            db.beginTransaction();

                            for (int j = isub.length - 1; j >= 0; j--)
                                try {
                                    long uid = ifolder.getUID(isub[j]);
                                    Log.i(Helper.TAG, "Boundary sync uid=" + uid);
                                    EntityMessage message = db.message().getMessageByUid(fid, uid);
                                    if (message == null) {
                                        ServiceSynchronize.synchronizeMessage(context, folder, ifolder, (IMAPMessage) isub[j], search != null);
                                        count++;
                                        loaded++;
                                    } else if (search != null) {
                                        existing.add(message.id);
                                        db.message().setMessageFound(message.id, true);
                                    }
                                } catch (MessageRemovedException ex) {
                                    Log.w(Helper.TAG, "Boundary " + ex + "\n" + Log.getStackTraceString(ex));
                                } catch (FolderClosedException ex) {
                                    throw ex;
                                } catch (FolderClosedIOException ex) {
                                    throw ex;
                                } catch (Throwable ex) {
                                    Log.e(Helper.TAG, "Boundary " + ex + "\n" + Log.getStackTraceString(ex));
                                } finally {
                                    ((IMAPMessage) isub[j]).invalidateHeaders();
                                }

                            db.setTransactionSuccessful();
                        } finally {
                            db.endTransaction();
                        }
                    }

                    Log.i(Helper.TAG, "Boundary done");
                } catch (final Throwable ex) {
                    Log.e(Helper.TAG, "Boundary " + ex + "\n" + Log.getStackTraceString(ex));
                    handler.post(new Runnable() {
                        @Override
                        public void run() {
                            intf.onError(context, ex);
                        }
                    });
                } finally {
                    searching = false;
                    handler.post(new Runnable() {
                        @Override
                        public void run() {
                            intf.onLoaded();
                        }
                    });
                }
            }
        });
    }
}