/*
 * @copyright (c) 2016, Philipp Thürwächter & Pattrick Hüper
 * @copyright (c) 2007-present, Stephen Colebourne & Michael Nascimento Santos
 * @license BSD-3-Clause (see LICENSE in the root directory of this source tree)
 */

import {assert, requireNonNull} from '../assert';

import {DateTimeBuilder} from './DateTimeBuilder';
import {EnumMap} from './EnumMap';

import {IsoChronology} from '../chrono/IsoChronology';
import {Temporal} from '../temporal/Temporal';
import {TemporalQueries} from '../temporal/TemporalQueries';

/**
 * @private
 */
export class DateTimeParseContext{

    constructor(){
        if(arguments.length === 1){
            if(arguments[0] instanceof DateTimeParseContext){
                this._constructorSelf.apply(this, arguments);
                return;
            } else {
                this._constructorFormatter.apply(this, arguments);
            }
        } else {
            this._constructorParam.apply(this, arguments);
        }

        this._caseSensitive = true;
        this._strict = true;
        this._parsed = [new Parsed(this)];
    }

    _constructorParam(locale, symbols, chronology){
        this._locale = locale;
        this._symbols = symbols;
        this._overrideChronology = chronology;
    }

    _constructorFormatter(formatter){
        this._locale = formatter.locale();
        this._symbols = formatter.decimalStyle();
        this._overrideChronology = formatter.chronology();
    }


    _constructorSelf(other) {
        this._locale = other._locale;
        this._symbols = other._symbols;
        this._overrideChronology = other._overrideChronology;
        this._overrideZone = other._overrideZone;
        this._caseSensitive = other._caseSensitive;
        this._strict = other._strict;
        this._parsed = [new Parsed(this)];
    }

    /**
     * Creates a copy of this context.
     */
    copy() {
        return new DateTimeParseContext(this);
    }

    symbols(){
        return this._symbols;
    }

    isStrict(){
        return this._strict;
    }

    setStrict(strict){
        this._strict = strict;
    }

    locale() {
        return this._locale;
    }

    setLocale(locale) {
        this._locale = locale;
    }
    //-----------------------------------------------------------------------
    /**
     * Starts the parsing of an optional segment of the input.
     */
    startOptional() {
        this._parsed.push(this.currentParsed().copy());
    }

    /**
     * Ends the parsing of an optional segment of the input.
     *
     * @param {boolean} successful  whether the optional segment was successfully parsed
     */
    endOptional(successful) {
        if (successful) {
            this._parsed.splice(this._parsed.length - 2, 1);
        } else {
            this._parsed.splice(this._parsed.length - 1, 1);
        }
    }

    /**
     * Checks if parsing is case sensitive.
     *
     * @return true if parsing is case sensitive, false if case insensitive
     */
    isCaseSensitive() {
        return this._caseSensitive;
    }

    /**
     * Sets whether the parsing is case sensitive or not.
     *
     * @param caseSensitive  changes the parsing to be case sensitive or not from now on
     */
    setCaseSensitive(caseSensitive) {
        this._caseSensitive = caseSensitive;
    }

    /**
     * Helper to compare two {@link CharSequence} instances.
     * This uses {@link isCaseSensitive}.
     *
     * @param cs1  the first character sequence, not null
     * @param offset1  the offset into the first sequence, valid
     * @param cs2  the second character sequence, not null
     * @param offset2  the offset into the second sequence, valid
     * @param length  the length to check, valid
     * @return true if equal
     */
    subSequenceEquals(cs1, offset1, cs2, offset2, length) {
        if (offset1 + length > cs1.length || offset2 + length > cs2.length) {
            return false;
        }
        if (! this.isCaseSensitive()) {
            cs1 = cs1.toLowerCase();
            cs2 = cs2.toLowerCase();
        }
        for (let i = 0; i < length; i++) {
            const ch1 = cs1[offset1 + i];
            const ch2 = cs2[offset2 + i];
            if (ch1 !== ch2) {
                return false;
            }
        }
        return true;
    }

    /**
     * Helper to compare two `char`.
     * This uses {@link isCaseSensitive}.
     *
     * @param ch1  the first character
     * @param ch2  the second character
     * @return true if equal
     */
    charEquals(ch1, ch2) {
        if (this.isCaseSensitive()) {
            return ch1 === ch2;
        }
        return this.charEqualsIgnoreCase(ch1, ch2);
    }

    /**
     * Compares two characters ignoring case.
     *
     * @param c1  the first
     * @param c2  the second
     * @return true if equal
     */
    charEqualsIgnoreCase(c1, c2) {
        return c1 === c2 ||
                c1.toLowerCase() === c2.toLowerCase();
    }

    setParsedField(field, value, errorPos, successPos){
        const currentParsedFieldValues = this.currentParsed().fieldValues;
        const old = currentParsedFieldValues.get(field);
        currentParsedFieldValues.set(field, value);
        return (old != null && old !== value) ? ~errorPos : successPos;
    }

    /**
     * Stores the parsed zone.
     *
     * This stores the zone that has been parsed.
     * No validation is performed other than ensuring it is not null.
     *
     * @param {ZoneId} zone  the parsed zone, not null
     */
    setParsedZone(zone) {
        requireNonNull(zone, 'zone');
        this.currentParsed().zone = zone;
    }

    getParsed(field) {
        return this.currentParsed().fieldValues.get(field);
    }

    toParsed() {
        return this.currentParsed();
    }

    currentParsed() {
        return this._parsed[this._parsed.length - 1];
    }

    /**
     * Stores the leap second.
     */
    setParsedLeapSecond() {
        this.currentParsed().leapSecond = true;
    }

    /**
     * Gets the effective chronology during parsing.
     *
     * @return the effective parsing chronology, not null
     */
    getEffectiveChronology() {
        let chrono = this.currentParsed().chrono;
        if (chrono == null) {
            chrono = this._overrideChronology;
            if (chrono == null) {
                chrono = IsoChronology.INSTANCE;
            }
        }
        return chrono;
    }


}

class Parsed extends Temporal {
    constructor(dateTimeParseContext){
        super();
        this.chrono = null;
        this.zone = null;
        this.fieldValues = new EnumMap();
        this.leapSecond = false;
        this.dateTimeParseContext = dateTimeParseContext;
    }

    copy() {
        const cloned = new Parsed();
        cloned.chrono = this.chrono;
        cloned.zone = this.zone;
        cloned.fieldValues.putAll(this.fieldValues);
        cloned.leapSecond = this.leapSecond;
        cloned.dateTimeParseContext = this.dateTimeParseContext;
        return cloned;
    }

    toString() {
        return `${this.fieldValues}, ${this.chrono}, ${this.zone}`;
    }

    isSupported(field) {
        return this.fieldValues.containsKey(field);
    }

    get(field) {
        const val = this.fieldValues.get(field);
        assert(val != null);
        return val;
    }

    query(query) {
        if (query === TemporalQueries.chronology()) {
            return this.chrono;
        }
        if (query === TemporalQueries.zoneId() || query === TemporalQueries.zone()) {
            return this.zone;
        }
        return super.query(query);
    }

    toBuilder() {
        const builder = new DateTimeBuilder();
        builder.fieldValues.putAll(this.fieldValues);
        builder.chrono = this.dateTimeParseContext.getEffectiveChronology();
        if (this.zone != null) {
            builder.zone = this.zone;
        } else {
            builder.zone = this.overrideZone;
        }
        builder.leapSecond = this.leapSecond;
        builder.excessDays = this.excessDays;
        return builder;
    }
}
