/**
 * Copyright 2011-2021 Asakusa Framework Team.
 *
 * 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 com.asakusafw.runtime.flow;

import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;

import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.text.MessageFormat;

import org.apache.hadoop.io.Writable;
import org.junit.Test;

import com.asakusafw.runtime.model.DataModel;
import com.asakusafw.runtime.value.IntOption;

/**
 * Test for {@link FileMapListBuffer}.
 */
public class FileMapListBufferTest {

    /**
     * creates an empty buffer.
     */
    @Test
    public void createEmpty() {
        FileMapListBuffer<Holder> buf = new FileMapListBuffer<>();
        buf.begin();
        buf.end();
        assertThat(buf.size(), is(0));

        buf.shrink();
    }

    /**
     * creates a buffer with one element.
     */
    @Test
    public void createSingle() {
        FileMapListBuffer<Holder> buf = new FileMapListBuffer<>();
        buf.begin();
        assertThat(buf.isExpandRequired(), is(true));
        buf.expand(new Holder(""));
        assertThat(buf.isExpandRequired(), is(false));
        buf.advance().value = "Hello";
        buf.end();
        assertThat(buf.size(), is(1));
        assertThat(buf.get(0), is(new Holder("Hello")));

        buf.shrink();
    }

    /**
     * reuses element objects.
     */
    @Test
    public void reuse() {
        FileMapListBuffer<Holder> buf = new FileMapListBuffer<>();
        buf.begin();
        assertThat(buf.isExpandRequired(), is(true));
        buf.expand(new Holder(""));
        buf.advance().value = "Hello";
        buf.end();
        buf.shrink();

        buf.begin();
        buf.advance().value = "World";
        buf.end();

        assertThat(buf.size(), is(1));
        assertThat(buf.get(0), is(new Holder("World")));
        buf.shrink();
    }

    /**
     * creates a big buffer.
     */
    @Test
    public void createBigList() {
        int size = 10000000;
        long begin = System.currentTimeMillis();
        ListBuffer<Holder> buf = new FileMapListBuffer<>();
        buf.begin();
        for (int i = 0; i < size; i++) {
            if (buf.isExpandRequired()) {
                buf.expand(new Holder(""));
            }
            buf.advance().value = String.valueOf(i);
        }
        buf.end();
        long written = System.currentTimeMillis();
        System.out.println(MessageFormat.format(
                "Elapsed for write: {0}ms",
                written - begin));

        for (int i = 0; i < size; i++) {
            buf.get(i);
        }
        long read = System.currentTimeMillis();
        System.out.println(MessageFormat.format(
                "Elapsed for read: {0}ms",
                read - written));

        assertThat(buf.size(), is(size));

        for (int i = 0; i < size; i++) {
            assertThat(buf.get(i).value, is(String.valueOf(i)));
        }

        buf.shrink();
    }

    /**
     * w/ multiple records.
     * @throws Exception if failed
     */
    @Test
    public void huge() throws Exception {
        long t0 = System.currentTimeMillis();
        ListBuffer<IntOption> list = new FileMapListBuffer<>();
        try {
            int begin = 0;
            int end = 100_000_000;
            int size = range(list, begin, end);
            assertThat(list.size(), is(size));
            long t1 = System.currentTimeMillis();
            for (int i = 0, n = end - begin; i < n; i++) {
                IntOption value = list.get(i);
                assertEquals(i + begin, value.get());
            }
            long t2 = System.currentTimeMillis();
            System.out.printf("SpLB - write: %,dms, read: %,dms%n", t1 - t0, t2 - t1);
        } finally {
            list.shrink();
        }
    }

    @SuppressWarnings("deprecation")
    private static int range(ListBuffer<IntOption> buffer, int begin, int end) {
        buffer.begin();
        for (int i = begin; i < end; i++) {
            if (buffer.isExpandRequired()) {
                buffer.expand(new IntOption());
            }
            buffer.advance().modify(i);
        }
        buffer.end();
        return end - begin;
    }

    /**
     * over expand.
     */
    @Test(expected = IndexOutOfBoundsException.class)
    public void over_expand() {
        FileMapListBuffer<Holder> buf = new FileMapListBuffer<>();
        try {
            buf.begin();
            while (true) {
                buf.expand(new Holder(""));
            }
        } finally {
            buf.shrink();
        }
    }

    /**
     * missing advance.
     */
    @Test(expected = IndexOutOfBoundsException.class)
    public void get_UpperOutOfBounds() {
        FileMapListBuffer<Holder> buf = new FileMapListBuffer<>();
        try {
            buf.begin();
            buf.end();
            buf.get(0);
        } finally {
            buf.shrink();
        }
    }

    /**
     * negative index.
     */
    @Test(expected = IndexOutOfBoundsException.class)
    public void get_LowerOutOfBounds() {
        FileMapListBuffer<Holder> buf = new FileMapListBuffer<>();
        try {
            buf.begin();
            buf.end();
            buf.get(-1);
        } finally {
            buf.shrink();
        }
    }

    static class Holder implements DataModel<Holder>, Writable {

        String value;

        Holder(String value) {
            this.value = value;
        }

        @Override
        public int hashCode() {
            final int prime = 31;
            int result = 1;
            result = prime * result + ((value == null) ? 0 : value.hashCode());
            return result;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            Holder other = (Holder) obj;
            if (value == null) {
                if (other.value != null) {
                    return false;
                }
            } else if (!value.equals(other.value)) {
                return false;
            }
            return true;
        }

        @Override
        public String toString() {
            return String.valueOf(value);
        }

        @Override
        public void write(DataOutput out) throws IOException {
            if (value != null) {
                out.writeBoolean(true);
                out.writeUTF(value);
            } else {
                out.writeBoolean(false);
            }
        }

        @Override
        public void readFields(DataInput in) throws IOException {
            if (in.readBoolean()) {
                value = in.readUTF();
            } else {
                value = null;
            }
        }

        @Override
        public void reset() {
            value = null;
        }

        @Override
        public void copyFrom(Holder other) {
            value = other.value;
        }
    }
}
