/**
 * This file is part of the Meeds project (https://meeds.io/).
 *
 * Copyright (C) 2020 - 2025 Meeds Association contact@meeds.io
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 3 of the License, or (at your option) any later version.
 * This program 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */
package org.exoplatform.portal.tree.diff;

import java.util.Iterator;
import java.util.NoSuchElementException;


public class HierarchyChangeIterator<L1, N1, L2, N2, H> implements Iterator<HierarchyChangeType> {

    /** . */
    private final HierarchyDiff<L1, N1, L2, N2, H> diff;

    /** . */
    private Frame frame;

    /** . */
    private final HierarchyContext<L1, N1, H> context1;

    /** . */
    private final HierarchyContext<L2, N2, H> context2;

    /** . */
    private final ListDiff<L1, L2, H> listDiff;

    HierarchyChangeIterator(HierarchyDiff<L1, N1, L2, N2, H> diff, HierarchyContext<L1, N1, H> context1,
            HierarchyContext<L2, N2, H> context2) {
        this.diff = diff;
        this.context1 = context1;
        this.context2 = context2;
        this.frame = new Frame(null, context1.getRoot(), context2.getRoot());
        this.listDiff = new ListDiff<L1, L2, H>(diff.listAdapter1, diff.listAdapter2, diff.comparator);
    }

    /**
     * The internal status.
     */
    private enum Status {

        INIT(null),

        ENTER(HierarchyChangeType.ENTER),

        KEEP(HierarchyChangeType.KEEP),

        ADDED(HierarchyChangeType.ADDED),

        REMOVED(HierarchyChangeType.REMOVED),

        MOVED_IN(HierarchyChangeType.MOVED_IN),

        MOVED_OUT(HierarchyChangeType.MOVED_OUT),

        LEAVE(HierarchyChangeType.LEAVE),

        ERROR(HierarchyChangeType.ERROR),

        RESUME(null);

        /** The associated change type. */
        final HierarchyChangeType changeType;

        private Status(HierarchyChangeType changeType) {
            this.changeType = changeType;
        }
    }

    private class Frame {

        /** . */
        private final Frame parent;

        /** . */
        private final N1 srcRoot;

        /** . */
        private final N2 dstRoot;

        /** . */
        private ListChangeIterator<L1, L2, H> it;

        /** . */
        private Status previous;

        /** . */
        private Status next;

        /** . */
        private Iterator<H> srcIt;

        /** . */
        private Iterator<H> dstIt;

        /** . */
        private N1 src;

        /** . */
        private N2 dst;

        private Frame(Frame parent, N1 srcRoot, N2 dstRoot) {
            this.parent = parent;
            this.srcRoot = srcRoot;
            this.dstRoot = dstRoot;
            this.previous = Status.INIT;
        }
    }

    public boolean hasNext() {
        if (frame != null && frame.next == null) {
            while (true) {

                if (frame.previous == Status.INIT) {
                    H id2 = context2.getHierarchyAdapter().getHandle(frame.dstRoot);
                    if (frame.srcRoot == null) {
                        frame.next = Status.ENTER;
                        frame.src = null;
                        frame.dst = frame.dstRoot;
                    } else {
                        H id1 = context1.getHierarchyAdapter().getHandle(frame.srcRoot);
                        if (diff.comparator.compare(id1, id2) != 0) {
                            frame.next = Status.ERROR;
                            frame.src = frame.srcRoot;
                            frame.dst = frame.dstRoot;
                        } else {
                            frame.next = Status.ENTER;
                            frame.src = frame.srcRoot;
                            frame.dst = frame.dstRoot;
                        }
                    }
                    break;
                } else if (frame.previous == Status.ERROR) {
                    break;
                } else if (frame.previous == Status.LEAVE) {
                    frame = frame.parent;
                    if (frame != null) {
                        frame.previous = Status.RESUME;
                        continue;
                    } else {
                        break;
                    }
                } else if (frame.previous == Status.KEEP) {
                    frame = new Frame(frame, frame.src, frame.dst);
                    continue;
                } else if (frame.previous == Status.MOVED_IN) {
                    frame = new Frame(frame, frame.src, frame.dst);
                    continue;
                } else if (frame.previous == Status.ADDED) {
                    frame = new Frame(frame, frame.src, frame.dst);
                    continue;
                } else if (frame.previous == Status.ENTER) {
                    L1 children1;
                    if (frame.src != null) {
                        children1 = context1.getHierarchyAdapter().getChildren(frame.srcRoot);
                        frame.srcIt = diff.listAdapter1.iterator(children1, false);
                    } else {
                        children1 = null;
                        frame.srcIt = null;
                    }
                    L2 children2 = context2.getHierarchyAdapter().getChildren(frame.dstRoot);
                    frame.dstIt = diff.listAdapter2.iterator(children2, false);
                    frame.it = listDiff.iterator(children1, children2);
                } else {
                    // Nothing
                }

                //
                if (frame.it.hasNext()) {
                    switch (frame.it.next()) {
                        case SAME:
                            N1 next1 = context1.findByHandle(frame.srcIt.next());
                            N2 next2 = context2.findByHandle(frame.dstIt.next());
                            frame.next = Status.KEEP;
                            frame.src = next1;
                            frame.dst = next2;
                            break;
                        case ADD:
                            frame.dstIt.next();
                            H addedHandle = frame.it.getElement();
                            N2 added = context2.findByHandle(addedHandle);
                            H addedId = context2.getHierarchyAdapter().getHandle(added);
                            N1 a = context1.findByHandle(addedId);
                            if (a != null) {
                                frame.next = Status.MOVED_IN;
                                frame.src = a;
                                frame.dst = added;
                            } else {
                                frame.next = Status.ADDED;
                                frame.src = null;
                                frame.dst = added;
                            }
                            break;
                        case REMOVE:
                            frame.srcIt.next();
                            H removedHandle = frame.it.getElement();
                            N1 removed = context1.findByHandle(removedHandle);
                            H removedId = context1.getHierarchyAdapter().getHandle(removed);
                            N2 b = context2.findByHandle(removedId);
                            if (b != null) {
                                frame.next = Status.MOVED_OUT;
                                frame.src = removed;
                                frame.dst = b;
                            } else {
                                frame.next = Status.REMOVED;
                                frame.src = removed;
                                frame.dst = null;
                            }
                            break;
                        default:
                            throw new AssertionError();
                    }
                } else {
                    frame.next = Status.LEAVE;
                    frame.src = frame.srcRoot;
                    frame.dst = frame.dstRoot;
                }

                //
                break;
            }
        }
        return frame != null && frame.next != null;
    }

    public HierarchyChangeType next() {
        if (!hasNext()) {
            throw new NoSuchElementException();
        } else {
            frame.previous = frame.next;
            frame.next = null;
            return frame.previous.changeType;
        }
    }

    public void skip() {
        if (frame.previous == HierarchyChangeIterator.Status.ENTER) {

            // A bit hackish as it bypass the main loop
            // the proper way to do it would be to introduce a SKIP status
            // and properly react to it to update the state machine
            // but for now it will do

            frame.next = Status.LEAVE;
            frame.src = frame.srcRoot;
            frame.dst = frame.dstRoot;
        } else {
            throw new IllegalStateException("Cannot skip when in state " + frame.previous);
        }
    }

    public N1 getSource() {
        return frame.src;
    }

    public N2 getDestination() {
        return frame.dst;
    }

    public N1 peekSourceRoot() {
        return frame.srcRoot;
    }

    public N2 peekDestinationRoot() {
        return frame.dstRoot;
    }

    public void remove() {
        throw new UnsupportedOperationException();
    }
}
