package ru.yandex.webmaster3.worker.queue;

import java.util.Base64;
import java.util.BitSet;

import org.joda.time.DateTime;

/**
 * @author aherman
 */
class QueueChunk {
    private final long chunkDateMs;
    private final int chunkCount;
    private ChunkState state;

    private TaskRunData[] taskList;
    private BitSet flags;
    private int capacity;
    private int tail;

    public QueueChunk(int chunkCount, int maxTasks) {
        this.chunkDateMs = System.currentTimeMillis();
        this.chunkCount = chunkCount;

        this.state = ChunkState.WRITABLE;
        this.taskList = new TaskRunData[maxTasks];
        this.flags = new BitSet(maxTasks);
        this.capacity = maxTasks;
        this.tail = 0;
    }

    public QueueChunk(long chunkDateMs, int chunkCount, BitSet flags) {
        this.chunkDateMs = chunkDateMs;
        this.chunkCount = chunkCount;
        this.flags = flags;
        this.state = ChunkState.UNLOADED;
        this.taskList = null;
        this.capacity = 0;
        this.tail = 0;
    }

    @Deprecated
    public QueueChunk(long chunkDateMs, int chunkCount, int head, int tail) {
        this.chunkDateMs = chunkDateMs;
        this.chunkCount = chunkCount;

        this.flags = new BitSet(tail);
        for (int i = head; i < tail; i++) {
            this.flags.set(i);
        }

        this.state = ChunkState.UNLOADED;
        this.taskList = null;
        this.capacity = 0;
        this.tail = tail;
    }

    public void clear() {
        //this.head = 0;
        //this.tail = 0;
        this.flags = new BitSet(this.flags.size());
        this.tail = 0;
    }

    public long getChunkDateMs() {
        return chunkDateMs;
    }

    public int getChunkCount() {
        return chunkCount;
    }

    int getTail() {
        return tail;
    }

    int getChunkSize() {
        return this.flags.cardinality();
    }

    TaskRunData[] getTaskList() {
        return taskList;
    }

    BitSet getFlags() {
        return flags;
    }

    public boolean isEmpty() {
        return this.flags.nextSetBit(0) < 0;
    }

    public DateTime getCreateDate() {
        return new DateTime(chunkDateMs);
    }

    public String getChunkId() {
        return "" + chunkDateMs + "." + chunkCount;
    }

    public ChunkState getState() {
        return state;
    }

    public boolean isFull() {
        return this.tail >= this.capacity;
    }

    public boolean hasNextTask() {
        return !isEmpty();
    }

    public boolean addTask(TaskRunData taskRunData) {
        if (state != ChunkState.WRITABLE) {
            throw new IllegalArgumentException("State must be " + ChunkState.WRITABLE);
        }

        if (isFull()) {
            return false;
        }

        int idx = tail++;
        taskList[idx] = taskRunData;
        flags.set(idx);
        return true;
    }

    public TaskRunData pollTask() throws InterruptedException {
        int idx = this.flags.nextSetBit(0);
        if (idx < 0) {
            return null;
        }

        this.flags.clear(idx);
        return taskList[idx];
    }

    public void setState(ChunkState state, TaskRunData[] taskList) {
        switch (state) {
            case WRITABLE:
                throw new IllegalStateException("Can not change state to " + state);

            case UNLOADED:
                if (taskList != null) {
                    throw new IllegalArgumentException("Task list must be null");
                }
                if (this.state == ChunkState.WRITABLE || this.state == ChunkState.LOADED) {
                    this.taskList = null;
                    this.capacity = 0;
                }
                break;

            case LOADED:
                if (this.state != ChunkState.UNLOADED) {
                    throw new IllegalStateException("Can not change state " + this.state + " to " + state);
                }
                if (taskList == null) {
                    throw new IllegalArgumentException("Chunk must not be null");
                }
                this.taskList = taskList;
                this.capacity = taskList.length;
                this.tail = this.capacity;

                for (int i = this.capacity; i < this.flags.size(); i++) {
                    this.flags.clear(i);
                }

                break;

            default:
                throw new IllegalStateException("Unknown state " + state);
        }
        this.state = state;
    }

    static enum ChunkState {
        WRITABLE,
        UNLOADED,
        LOADED
    }

    @Override
    public String toString() {
        return getChunkId() + "[" + flags.toString() +  "]";
    }

    static  class BitSetUtils {
        public static BitSet fromBase64String(String s) {
            return BitSet.valueOf(Base64.getDecoder().decode(s));
        }

        public static String toBase64String(BitSet bs) {
            return new String(Base64.getEncoder().encode(bs.toByteArray()));
        }

        public static BitSet transferBitSet(BitSet src, int length) {
            BitSet dst = new BitSet(length);
            int i = src.nextSetBit(0);
            while (i >= 0 && i < length) {
                dst.set(i);
                i = src.nextSetBit(i+1);
            }
            return dst;
        }
    }
}
