/*
 *  Copyright (C) 2010-2015 JPEXS, All rights reserved.
 *
 * This library 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.0 of the License, or (at your option) any later version.
 *
 * This library 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 library.
 */
package com.jpexs.decompiler.flash.action.fastactionlist;

import com.jpexs.decompiler.flash.action.Action;
import com.jpexs.decompiler.flash.action.ActionList;
import com.jpexs.decompiler.flash.action.special.ActionStore;
import com.jpexs.decompiler.flash.action.swf4.ActionIf;
import com.jpexs.decompiler.flash.action.swf4.ActionJump;
import com.jpexs.decompiler.flash.action.swf4.ActionPush;
import com.jpexs.decompiler.graph.GraphSourceItemContainer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 *
 * @author JPEXS
 */
public class FastActionList implements Collection<ActionItem> {

    private int size;

    private ActionItem firstItem;

    private final Map<Action, ActionItem> actionItemMap;

    private final Set<ActionItem> actionItemSet;

    public FastActionList(ActionList actions) {
        actionItemMap = new HashMap<>(actions.size());
        actionItemSet = new HashSet<>(actions.size());
        for (Action action : actions) {
            insertItemAfter(null, action);
        }

        size = actions.size();
        getContainerLastActions(actions, actionItemMap);
        getJumps(actions, actionItemMap);
    }

    public final ActionItem insertItemBefore(ActionItem item, Action action) {
        ActionItem newItem = new ActionItem(action);
        return insertItemBefore(item, newItem);
    }

    public final ActionItem insertItemAfter(ActionItem item, Action action) {
        ActionItem newItem = new ActionItem(action);
        return insertItemAfter(item, newItem);
    }

    public final ActionItem insertItemBefore(ActionItem item, ActionItem newItem) {
        insertItemAfter(item.prev, newItem);
        if (item == firstItem) {
            firstItem = newItem;
        }

        return newItem;
    }

    public final ActionItem insertItemAfter(ActionItem item, ActionItem newItem) {
        if (item == null && firstItem == null) {
            firstItem = newItem;
            newItem.next = newItem;
            newItem.prev = newItem;
        } else {
            if (item == null) {
                // insert to the end
                item = firstItem.prev;
            }

            ActionItem oldNext = item.next;
            newItem.prev = item;
            newItem.next = oldNext;
            item.next = newItem;
            oldNext.prev = newItem;
        }

        size++;
        actionItemMap.put(newItem.action, newItem);
        actionItemSet.add(newItem);
        return newItem;
    }

    public ActionItem removeItem(ActionItem item) {
        ActionItem next = null;
        if (item == firstItem) {
            if (item.next == item) {
                // there is only 1 item
                firstItem = null;
            } else {
                next = item.next;
                firstItem = next;
                next.prev = item.prev;
                item.prev.next = next;
            }
        } else {
            next = item.next;
            item.prev.next = next;
            next.prev = item.prev;
        }

        size--;
        actionItemMap.remove(item.action);
        actionItemSet.remove(item);

        item.removeJumpTarget();
        item.removeContainerLastActions();

        if (item.jumpsHere != null) {
            for (ActionItem item1 : new ArrayList<>(item.jumpsHere)) {
                item1.setJumpTarget(item.next);
            }
        }

        if (item.lastActionOf != null) {
            for (ActionItem item1 : new ArrayList<>(item.lastActionOf)) {
                item1.replaceContainerLastAction(item, item.prev);
            }
        }

        return next;
    }

    public void removeItem(int index, int count) {
        FastActionListIterator iterator = new FastActionListIterator(this, index);
        for (int i = 0; i < count; i++) {
            iterator.next();
            iterator.remove();
        }
    }

    public ActionItem get(int index) {
        FastActionListIterator iterator = new FastActionListIterator(this, index);
        return iterator.next();
    }

    public void replaceJumpTargets(ActionItem target, ActionItem newTarget) {
        if (target.jumpsHere != null) {
            for (ActionItem item : new ArrayList<>(target.jumpsHere)) {
                item.setJumpTarget(newTarget);
            }
        }
    }

    private void getContainerLastActions(ActionList actions, Map<Action, ActionItem> actionItemMap) {
        ActionItem item = firstItem;
        if (item == null) {
            return;
        }

        do {
            Action action = item.action;
            if (action instanceof GraphSourceItemContainer) {
                item.setContainerLastActions(getContainerLastActions(actions, action, actionItemMap));
            }

            item = item.next;
        } while (item != firstItem);
    }

    private List<ActionItem> getContainerLastActions(ActionList actions, Action action, Map<Action, ActionItem> actionItemMap) {
        GraphSourceItemContainer container = (GraphSourceItemContainer) action;
        List<Long> sizes = container.getContainerSizes();
        long endAddress = action.getAddress() + container.getHeaderSize();
        List<ActionItem> lasts = new ArrayList<>(sizes.size());
        for (long size : sizes) {
            endAddress += size;
            long lastActionAddress = getNearAddress(actions, endAddress - 1, false);
            Action lastAction = null;
            if (lastActionAddress != -1) {
                lastAction = actions.getByAddress(lastActionAddress);
            }

            if (lastAction != null) {
                lasts.add(actionItemMap.get(lastAction));
            } else {
                lasts.add(null);
            }
        }
        return lasts;
    }

    private long getNearAddress(ActionList actions, long address, boolean next) {
        int min = 0;
        int max = actions.size() - 1;

        while (max >= min) {
            int mid = (min + max) / 2;
            long midValue = actions.get(mid).getAddress();
            if (midValue == address) {
                return address;
            } else if (midValue < address) {
                min = mid + 1;
            } else {
                max = mid - 1;
            }
        }

        return next
                ? (min < actions.size() ? actions.get(min).getAddress() : -1)
                : (max >= 0 ? actions.get(max).getAddress() : -1);
    }

    private void getJumps(ActionList actions, Map<Action, ActionItem> actionItemMap) {
        ActionItem item = firstItem;
        if (item == null) {
            return;
        }

        do {
            Action action = item.action;
            long target = -1;
            if (action instanceof ActionIf) {
                target = ((ActionIf) action).getTargetAddress();
            } else if (action instanceof ActionJump) {
                target = ((ActionJump) action).getTargetAddress();
            } else if (action instanceof ActionStore) {
                ActionStore aStore = (ActionStore) action;
                int storeSize = aStore.getStoreSize();
                // skip storeSize + 1 actions (+1 is the current action)
                Action targetAction = action;
                for (int i = 0; i <= storeSize; i++) {
                    long address = targetAction.getAddress() + targetAction.getTotalActionLength();
                    targetAction = actions.getByAddress(address);
                    if (targetAction == null) {
                        break;
                    }
                }

                item.setJumpTarget(actionItemMap.get(targetAction));
            }
            if (target >= 0) {
                Action targetAction = actions.getByAddress(target);
                item.setJumpTarget(actionItemMap.get(targetAction));
            }

            item = item.next;
        } while (item != firstItem);
    }

    private void updateActionAddressesAndLengths() {
        ActionItem item = firstItem;
        if (item == null) {
            return;
        }

        long address = item.action.getAddress();
        do {
            Action action = item.action;
            action.setAddress(address);
            action.updateLength();
            address += action.getTotalActionLength();
            item = item.next;
        } while (item != firstItem);
    }

    private void updateJumps() {
        ActionItem item = firstItem;
        if (item == null) {
            return;
        }

        long endAddress = item.prev.action.getAddress();
        do {
            Action action = item.action;
            if (action instanceof ActionIf) {
                ActionIf aIf = (ActionIf) action;
                Action target = item.getJumpTargetAction();
                long offset;
                if (target != null) {
                    offset = target.getAddress() - action.getAddress() - action.getTotalActionLength();
                } else {
                    offset = endAddress - action.getAddress() - action.getTotalActionLength();
                }
                aIf.setJumpOffset((int) offset);
            } else if (action instanceof ActionJump) {
                ActionJump aJump = (ActionJump) action;
                Action target = item.getJumpTargetAction();
                long offset;
                if (target != null) {
                    offset = target.getAddress() - action.getAddress() - action.getTotalActionLength();
                } else {
                    offset = endAddress - action.getAddress() - action.getTotalActionLength();
                }
                aJump.setJumpOffset((int) offset);
            }

            item = item.next;
        } while (item != firstItem);
    }

    private void updateActionStores() {
        ActionItem item = firstItem;
        if (item == null) {
            return;
        }

        do {
            Action action = item.action;
            if (action instanceof ActionStore) {
                ActionStore aStore = (ActionStore) action;
                Action nextActionAfterStore = item.getJumpTargetAction();
                ActionItem item1 = item;
                List<Action> store = new ArrayList<>();
                while (true) {
                    item1 = item1.next;
                    if (item1 == firstItem || item1.action == nextActionAfterStore) {
                        break;
                    }

                    store.add(item1.action);
                }

                aStore.setStore(store);
            }

            item = item.next;
        } while (item != firstItem);
    }

    private void updateContainerSizes() {
        ActionItem item = firstItem;
        if (item == null) {
            return;
        }

        do {
            Action action = item.action;
            if (action instanceof GraphSourceItemContainer) {
                GraphSourceItemContainer container = (GraphSourceItemContainer) action;
                List<ActionItem> lastActions = item.getContainerLastActions();
                long startAddress = action.getAddress() + container.getHeaderSize();
                for (int j = 0; j < lastActions.size(); j++) {
                    Action lastAction = lastActions.get(j).action;
                    int length = (int) (lastAction.getAddress() + lastAction.getTotalActionLength() - startAddress);
                    container.setContainerSize(j, length);
                    startAddress += length;
                }
            }

            item = item.next;
        } while (item != firstItem);
    }

    public ActionItem getContainer(ActionItem item) {
        while (!(item.action instanceof GraphSourceItemContainer) && item != firstItem) {
            item = item.prev;
        }

        if (item.action instanceof GraphSourceItemContainer) {
            return item;
        }

        return null;
    }

    public void expandPushes() {
        ActionItem item = firstItem;
        if (item == null) {
            return;
        }

        do {
            Action action = item.action;
            if (action instanceof ActionPush) {
                ActionPush push = (ActionPush) action;
                if (push.values.size() > 1) {
                    for (int i = 1; i < push.values.size(); i++) {
                        Object value = push.values.get(i);
                        ActionPush newPush = new ActionPush(value);
                        newPush.constantPool = push.constantPool;
                        insertItemAfter(item, newPush);
                        item = item.next;
                    }

                    Object obj = push.values.get(0);
                    push.values.clear();
                    push.values.add(obj);
                }
            }

            item = item.next;
        } while (item != firstItem);
    }

    public void removeZeroJumps() {
        ActionItem item = firstItem;
        if (item == null) {
            return;
        }

        do {
            Action action = item.action;
            if (action instanceof ActionJump) {
                if (item.getJumpTarget() == item.next && item.getJumpTarget() != firstItem) {
                    item = removeItem(item);
                    continue;
                }
            }

            item = item.next;
        } while (item != firstItem);
    }

    public void removeUnreachableActions() {
        ActionItem item = firstItem;
        if (item == null) {
            return;
        }

        updateReachableFlags(null, null);

        do {
            if (item.reachable == 0) {
                item = removeItem(item);
                continue;
            }

            item = item.next;
        } while (item != firstItem);
    }

    public void removeIncludedActions() {
        ActionItem item = firstItem;
        if (item == null) {
            return;
        }

        do {
            if (!item.excluded) {
                item = removeItem(item);
                continue;
            }

            item = item.next;
        } while (item != firstItem);
    }

    public int getUnreachableActionCount(ActionItem jump, ActionItem jumpTarget) {
        ActionItem item = firstItem;
        if (item == null) {
            return 0;
        }

        updateReachableFlags(jump, jumpTarget);
        jump.reachable = 0;

        int count = 0;
        do {
            if (item.reachable == 0) {
                count++;
            }

            item = item.next;
        } while (item != firstItem);

        return count;
    }

    private void clearReachableFlags() {
        ActionItem item = firstItem;
        if (item == null) {
            return;
        }

        do {
            item.reachable = 0;
            item = item.next;
        } while (item != firstItem);
    }

    public void setExcludedFlags(boolean value) {
        ActionItem item = firstItem;
        if (item == null) {
            return;
        }

        do {
            item.excluded = value;
            item = item.next;
        } while (item != firstItem);
    }

    private void updateReachableFlags(ActionItem jump, ActionItem jumpTarget) {
        if (firstItem == null) {
            return;
        }

        clearReachableFlags();

        firstItem.reachable = 1;
        ActionItem firstItem2 = firstItem;
        boolean modified = true;
        while (modified) {
            modified = false;
            ActionItem item = firstItem2;
            do {
                ActionItem next = item.next;
                //ActionItem alternativeNext = null;
                Action action = item.action;
                if (item.reachable == 1) {
                    item.reachable = 2;
                    modified = true;

                    if (item == firstItem2) {
                        firstItem2 = next;
                    }

                    if (item == jump) {
                        if (jumpTarget.reachable == 0) {
                            jumpTarget.reachable = 1;
                            //alternativeNext = jumpTarget;
                        }
                    } else {

                        if (!action.isExit() && !(action instanceof ActionJump)) {
                            if (next.reachable == 0) {
                                next.reachable = 1;
                            }
                        }

                        if (action instanceof GraphSourceItemContainer) {
                            for (ActionItem lastActionItem : item.getContainerLastActions()) {
                                if (lastActionItem != null && lastActionItem.next != null && lastActionItem.next.reachable == 0) {
                                    lastActionItem.next.reachable = 1;
                                    //alternativeNext = lastActionItem.next;
                                }
                            }
                        }

                        ActionItem target = item.getJumpTarget();
                        if (target != null) {
                            if (target.reachable == 0) {
                                target.reachable = 1;
                                //alternativeNext = target;
                            }
                        }
                    }
                }

                //item = alternativeNext == null || next.reachable == 1 ? next : alternativeNext;
                item = next;
            } while (item != firstItem);
        }
    }

    public ActionList updateActions() {
        List<Action> resultList = new ArrayList<>(size);
        ActionItem item = firstItem;
        if (item == null) {
            return new ActionList(resultList);
        }

        do {
            resultList.add(item.action);
            item = item.next;
        } while (item != firstItem);

        ActionList result = new ActionList(resultList);
        updateActionAddressesAndLengths();
        updateJumps();
        updateActionStores();
        updateContainerSizes();
        return result;
    }

    public ActionItem first() {
        return firstItem;
    }

    public ActionItem last() {
        return firstItem == null ? null : firstItem.prev;
    }

    public ActionList toActionList() {
        return updateActions();
    }

    @Override
    public int size() {
        return size;
    }

    @Override
    public boolean isEmpty() {
        return size == 0;
    }

    @Override
    public boolean contains(Object o) {
        if (o instanceof ActionItem) {
            return actionItemSet.contains(o);
        } else if (o instanceof Action) {
            return actionItemMap.containsKey((Action) o);
        }

        return false;
    }

    @Override
    public FastActionListIterator iterator() {
        return new FastActionListIterator(this);
    }

    @Override
    public Object[] toArray() {
        Object[] result = new Object[size];

        ActionItem item = firstItem;
        if (item == null) {
            return result;
        }

        int i = 0;
        do {
            result[i] = item.action;
            item = item.next;
            i++;
        } while (item != firstItem);
        return null;
    }

    @Override
    @SuppressWarnings("unchecked")
    public <T> T[] toArray(T[] a) {
        if (a.length != size) {
            a = (T[]) new ActionItem[size];
        }

        ActionItem item = firstItem;
        if (item == null) {
            return a;
        }

        int i = 0;
        do {
            a[i] = (T) item;
            item = item.next;
            i++;
        } while (item != firstItem);
        return null;
    }

    @Override
    public boolean add(ActionItem e) {
        insertItemAfter(null, e);
        return true;
    }

    @Override
    public boolean remove(Object o) {
        ActionItem item = null;
        if (o instanceof ActionItem) {
            item = (ActionItem) o;
        } else if (o instanceof Action) {
            item = actionItemMap.get((Action) o);
        }

        if (item == null) {
            return false;
        }

        removeItem(item);
        return true;
    }

    @Override
    public boolean containsAll(Collection<?> c) {
        for (Object c1 : c) {
            if (!contains(c1)) {
                return false;
            }
        }

        return true;
    }

    @Override
    public boolean addAll(Collection<? extends ActionItem> c) {
        for (ActionItem c1 : c) {
            insertItemAfter(null, c1);
        }

        return true;
    }

    @Override
    public boolean removeAll(Collection<?> c) {
        boolean result = false;
        for (Object c1 : c) {
            result |= remove(c1);
        }

        return result;
    }

    @Override
    public boolean retainAll(Collection<?> c) {
        ActionItem item = firstItem;
        if (item == null) {
            return false;
        }

        boolean modified = false;
        do {
            if (!c.contains(item)) {
                item = removeItem(item);
                modified = true;
                continue;
            }

            item = item.next;
        } while (item != firstItem);
        return modified;
    }

    @Override
    public void clear() {
        firstItem = null;
        size = 0;
    }
}
