/*
 * Copyright (c) 2016—2017 Andrei Tomashpolskiy and individual contributors.
 *
 * 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 bt.bencoding;

import bt.bencoding.model.BEInteger;
import bt.bencoding.model.BEList;
import bt.bencoding.model.BEMap;
import bt.bencoding.model.BEObject;
import bt.bencoding.model.BEString;

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Objects;

/**
 * BEncoding parser. Should be closed when the source is processed.
 *
 * @since 1.0
 */
public class BEParser implements AutoCloseable {

    static final char EOF = 'e';

    static final char INTEGER_PREFIX = 'i';
    static final char LIST_PREFIX = 'l';
    static final char MAP_PREFIX = 'd';

    private Scanner scanner;
    private final BEType type;
    private Object parsedObject;

    /**
     * Create a parser for the provided URL's content.
     *
     * @param url URL's content must be a well-formed bencoded document.
     * @since 1.0
     */
    public BEParser(URL url) {
        Objects.requireNonNull(url, "Missing URL");
        try {
            this.scanner = new Scanner(url.openStream());
        } catch (IOException e) {
            throw new BtParseException("Failed to open stream for URL: " + url, new byte[0], e);
        }
        this.type = getTypeForPrefix((char) scanner.peek());
    }

    /**
     * Create a parser for the provided binary input.
     *
     * @param in Input's content must be a well-formed bencoded document.
     * @since 1.0
     */
    public BEParser(InputStream in) {
        Objects.requireNonNull(in, "Input stream is null");
        this.scanner = new Scanner(in);
        this.type = getTypeForPrefix((char) scanner.peek());
    }

    /**
     * Create a parser for the provided bencoded document.
     *
     * @param bs Bencoded document.
     * @since 1.0
     */
    public BEParser(byte[] bs) {

        if (bs == null || bs.length == 0) {
            throw new IllegalArgumentException("Can't parse bytes array: null or empty");
        }
        this.scanner = new Scanner(bs);
        this.type = getTypeForPrefix((char) scanner.peek());
    }

    /**
     * Read type of the root object of the bencoded document that this parser was created for.
     *
     * @since 1.0
     */
    public BEType readType() {
        return type;
    }

    static char getPrefixForType(BEType type) {

        if (type == null) {
            throw new NullPointerException("Can't get prefix -- type is null");
        }

        switch (type) {
            case INTEGER:
                return INTEGER_PREFIX;
            case LIST:
                return LIST_PREFIX;
            case MAP:
                return MAP_PREFIX;
            default:
                throw new IllegalArgumentException("Unknown type: " + type.name().toLowerCase());
        }
    }

    static BEType getTypeForPrefix(char c) {
        if (Character.isDigit(c)) {
            return BEType.STRING;
        }
        switch (c) {
            case INTEGER_PREFIX: {
                return BEType.INTEGER;
            }
            case LIST_PREFIX: {
                return BEType.LIST;
            }
            case MAP_PREFIX: {
                return BEType.MAP;
            }
            default: {
                throw new IllegalStateException("Invalid type prefix: " + c);
            }
        }
    }

    static BEObjectBuilder<? extends BEObject<?>> builderForType(BEType type) {
        switch (type) {
            case STRING: {
                return new BEStringBuilder();
            }
            case INTEGER: {
                return new BEIntegerBuilder();
            }
            case LIST: {
                return new BEListBuilder();
            }
            case MAP: {
                return new BEMapBuilder();
            }
            default: {
                throw new IllegalStateException("Unknown type: " + type.name().toLowerCase());
            }
        }
    }

    /**
     * Try to read the document's root object as a bencoded string.
     *
     * @see #readType()
     * @since 1.0
     */
    public BEString readString() {
        return readObject(BEType.STRING, BEStringBuilder.class);
    }

    /**
     * Try to read the document's root object as a bencoded integer.
     *
     * @see #readType()
     * @since 1.0
     */
    public BEInteger readInteger() {
        return readObject(BEType.INTEGER, BEIntegerBuilder.class);
    }

    /**
     * Try to read the document's root object as a bencoded list.
     *
     * @see #readType()
     * @since 1.0
     */
    public BEList readList() {
        return readObject(BEType.LIST, BEListBuilder.class);
    }

    /**
     * Try to read the document's root object as a bencoded dictionary.
     *
     * @see #readType()
     * @since 1.0
     */
    public BEMap readMap() {
        return readObject(BEType.MAP, BEMapBuilder.class);
    }

    private <T extends BEObject> T readObject(BEType type, Class<? extends BEObjectBuilder<T>> builderClass) {

        assertType(type);

        @SuppressWarnings("unchecked")
        T result = (T) parsedObject;
        if (result == null) {
            try {
                // relying on the default constructor being present
                parsedObject = result = scanner.readObject(builderClass.newInstance());
            } catch (Exception e) {
                throw new BtParseException("Failed to read from encoded data", scanner.getScannedContents(), e);
            }
        }
        return result;
    }

    private void assertType(BEType type) {

        if (type == null) {
            throw new NullPointerException("Invalid type -- null");
        }

        if (this.type != type) {
            throw new IllegalStateException(
                    "Can't read " + type.name().toLowerCase() + " from: " + this.type.name().toLowerCase());
        }
    }

    @Override
    public void close() {
        if (scanner != null) {
            scanner.close();
        }
    }
}