/*
 * Copyright © 2016 Tinkoff Bank
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package ru.tinkoff.decoro;

import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;

import java.lang.reflect.Array;
import java.util.Iterator;

import ru.tinkoff.decoro.slots.Slot;

/**
 * @author Mikhail Artemev
 */
class SlotsList implements Iterable<Slot>, Parcelable {

    private int size = 0;

    private Slot firstSlot;
    private Slot lastSlot;

    public static SlotsList ofArray(@NonNull Slot[] slots) {
        SlotsList list = new SlotsList();

        list.size = slots.length;

        if (list.size == 0) {
            return list;
        }

        linkSlots(slots, list);

        return list;
    }

    private static void linkSlots(@NonNull Slot[] slots, SlotsList list) {
        list.firstSlot = new Slot(slots[0]);
        Slot prev = list.firstSlot;

        if (list.size == 1) {
            list.lastSlot = list.firstSlot;
        }

        // link slots
        for (int i = 1; i < slots.length; i++) {
            Slot next = new Slot(slots[i]);
            prev.setNextSlot(next);
            next.setPrevSlot(prev);

            prev = next;

            if (i == slots.length - 1) {
                list.lastSlot = next;
            }
        }
    }

    public SlotsList() {
    }

    public SlotsList(@NonNull SlotsList list) {
        if (!list.isEmpty()) {
            Slot previous = null;
            for (Slot slot : list) {
                final Slot newSlot = new Slot(slot);
                if (size == 0) {
                    this.firstSlot = newSlot;
                } else {
                    previous.setNextSlot(newSlot);
                    newSlot.setPrevSlot(previous);
                }

                previous = newSlot;
                size++;
            }

            this.lastSlot = previous;
        }
    }

    public boolean checkIsIndex(int position) {
        return 0 <= position && position < size;
    }

    public Slot getSlot(int index) {
        if (!checkIsIndex(index)) {
            return null;
        }

        Slot result;

        if (index < (size >> 1)) {
            // first half of a list
            result = firstSlot;
            for (int i = 0; i < index; i++) {
                result = result.getNextSlot();
            }
        } else {
            // second half of a list
            result = lastSlot;
            for (int i = size - 1; i > index; i--) {
                result = result.getPrevSlot();
            }
        }

        if (result == null) {
            throw new IllegalStateException("Slot inside the mask should not be null. But it is.");
        }

        return result;
    }

    /**
     * Inserts a slot on a specified position
     *
     * @param position index where new slot weill be placed should be >= 0 and <= size.
     * @param slot     slot ot insert. IMPORTANT: a copy of this slot will be inserted!
     * @return newly inserted slot (copy of the passed one)
     */
    public Slot insertSlotAt(final int position, @NonNull final Slot slot) {

        if (position < 0 || size < position) {
            throw new IndexOutOfBoundsException("New slot position should be inside the slots list. Or on the tail (position = size)");
        }

        final Slot toInsert = new Slot(slot);

        Slot currentSlot = getSlot(position);
        Slot leftNeighbour;
        Slot rightNeighbour = null;
        if (currentSlot == null) {
            // this can happen only when position == size.
            // it means we want to add the slot on the tail
            leftNeighbour = lastSlot;
        } else {
            leftNeighbour = currentSlot.getPrevSlot();
            rightNeighbour = currentSlot;
        }

        toInsert.setNextSlot(rightNeighbour);
        toInsert.setPrevSlot(leftNeighbour);

        if (rightNeighbour != null) {
            // right neighbour is only available for non-last slots
            rightNeighbour.setPrevSlot(toInsert);
        }

        if (leftNeighbour != null) {
            // left neighbour is only available for not-first slots
            leftNeighbour.setNextSlot(toInsert);
        }

        if (position == 0) {
            firstSlot = toInsert;
        } else if (position == size) {
            lastSlot = toInsert;
        }

        size++;

        return toInsert;
    }

    public Slot removeSlotAt(int position) {
        if (!checkIsIndex(position)) {
            throw new IndexOutOfBoundsException("Slot position should be inside the slots list");
        }
        return removeSlot(getSlot(position));
    }

    public Slot removeSlot(final Slot slotToRemove) {
        if (slotToRemove == null || !contains(slotToRemove)) {
            return null;
        }

        Slot left = slotToRemove.getPrevSlot();
        Slot right = slotToRemove.getNextSlot();

        if (left != null) {
            left.setNextSlot(right);
        } else {
            firstSlot = right;
        }

        if (right != null) {
            right.setPrevSlot(left);
        } else {
            lastSlot = left;
        }

        size--;

        return slotToRemove;
    }

    public void clear() {
        if (isEmpty()) {
            return;
        }

        // iterate back because by default slots will try to pull new value from next slot
        // when someone try to remove their value
        Slot slot = lastSlot;
        while (slot != null) {
            slot.setValue(null);
            slot = slot.getPrevSlot();
        }
    }

    @Override
    public Iterator<Slot> iterator() {
        return new SlotsIterator(firstSlot);
    }

    @NonNull
    public Slot[] toArray() {
        if (isEmpty()) {
            return new Slot[0];
        }

        return toArray(new Slot[size()]);
    }

    @NonNull
    public <T> T[] toArray(@NonNull T[] array) {
        if (array == null || array.length < size) {
            array = (T[]) Array.newInstance(array.getClass().getComponentType(), size);
        }

        int index = 0;
        Object[] result = array;
        for (Slot slot : this) {
            result[index++] = slot;
        }

        return array;
    }

    public boolean add(Slot slot) {
        return insertSlotAt(size, slot) == slot;
    }


    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false;

        final SlotsList list = (SlotsList) obj;
        if (list.size() != size()) return false;

        final Iterator<Slot> ourIterator = iterator();
        for (Slot otherSlot : list) {
            if (!ourIterator.next().equals(otherSlot)) {
                return false;
            }
        }

        return true;
    }

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

    public int size() {
        return size;
    }

    public Slot getFirstSlot() {
        return firstSlot;
    }

    public void setFirstSlot(Slot firstSlot) {
        this.firstSlot = firstSlot;
    }

    public Slot getLastSlot() {
        return lastSlot;
    }

    public void setLastSlot(Slot lastSlot) {
        this.lastSlot = lastSlot;
    }

    private boolean contains(Slot o) {
        for (Slot slot : this) {
            if (slot == o) {
                return true;
            }
        }

        return false;
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeInt(this.size);
        if (size > 0) {
            dest.writeTypedArray(toArray(), flags);
        }
    }

    protected SlotsList(Parcel in) {
        this.size = in.readInt();
        if (size > 0) {
            Slot[] slots = new Slot[this.size];
            in.readTypedArray(slots, Slot.CREATOR);
            linkSlots(slots, this);
        }
    }

    public static final Creator<SlotsList> CREATOR = new Creator<SlotsList>() {
        @Override
        public SlotsList createFromParcel(Parcel source) {
            return new SlotsList(source);
        }

        @Override
        public SlotsList[] newArray(int size) {
            return new SlotsList[size];
        }
    };

    private static class SlotsIterator implements Iterator<Slot> {

        Slot nextSlot;

        public SlotsIterator(Slot currentSlot) {
            if (currentSlot == null) {
                throw new IllegalArgumentException("Initial slot for iterator cannot be null");
            }

            this.nextSlot = currentSlot;
        }

        @Override
        public boolean hasNext() {
            return nextSlot != null;
        }

        @Override
        public Slot next() {
            Slot current = nextSlot;
            nextSlot = nextSlot.getNextSlot();
            return current;
        }

        @Override
        public void remove() {
            throw new UnsupportedOperationException("Mask cannot be modified from outside!");
        }
    }
}