import { List, Record } from 'immutable';

import { DONE, FAIL } from '../constants';
import StateMachine, { State, StateMachineEnvironment } from './state-machine';
import {
    transformGeneric,
    QualifiedElement,
    CharClass,
    CharRange,
    NumericQualifier,
    StartOfString,
    EndOfString,
    Wildcard,
    Sequence,
} from './regex-ast';


/**
 * This is the top-level Regex class. Structurally, it is very similar to a Sequence.
 * The only difference is that Regex element lists are the only lists that can contain Regex.startOfString (^) and Regex.endOfString ($) elements.
 * Other than that, the Regex() constructor can be used to manually construct a Regex syntax tree,
 * and Regex.parse() can be used to parse a Regex syntax tree out of a valid JS regex or string.
 * A Regex object is completely immutable, from top to bottom, using ImmutableJS Records, Lists, and Maps.
 * Any ImmutableJS Record methods are accessible on a Regex object.
 */
export default class Regex extends Record({ elements: List() }) {
    constructor(...elements) {
        super({ elements: List(elements) });
    }

    withElement(element) {
        return this.updateIn(['elements'], el => el.push(element));
    }

    wrapPrevious(qualifier) {
        return this.updateIn(['elements', this.elements.size - 1], el => new QualifiedElement(el, qualifier));
    }

    static _parse(regex) {
        if (typeof regex === 'string') {
            // string regex, just parse it
            return regexStateMachine.run(regex); // eslint-disable-line no-use-before-define
        } else if (regex instanceof RegExp) {
            // JS regex, verify there are no flags and parse the source
            if (regex.flags) {
                throw new Error('Regex flags are not supported by InterParse. Stopping here to prevent unexpected behavior.');
            }
            return regexStateMachine.run(regex.source); // eslint-disable-line no-use-before-define
        }
        // there is no way this can ever be a regex
        throw new Error(`Invalid regex representation: ${regex}`);
    }

    static parse(regex, reduce = true) {
        const nfa = Regex._parse(regex)._transform();
        return reduce ? nfa.reduce() : nfa;
    }

    /**
     * A full regex transform is basically the same as a Sequence transform,
     * but we add some extra logic to handle the start and end characters.
     * We also need to make sure that the last state is a complete state.
     */
    _transform() { // #TODO handle start and end properly
        // setup first state and state machine
        const startState = StateMachine.generateStateId();
        let stateMachine = new StateMachine(startState).withState(startState, new State());
        // apply each element
        let previousStateId = startState;
        let nextStateId;
        stateMachine = this.elements.reduce((sm, element, i) => {
            nextStateId = StateMachine.generateStateId();
            return transformGeneric(sm, element, previousStateId, previousStateId = nextStateId);
        }, stateMachine);
        // make the last state a complete state
        return stateMachine.withState(nextStateId, new State({ isCompleteState: true }));
    }
}

// built-in character classes
Regex.decimal = new CharClass(new CharRange('0', '9'));
Regex.nonDecimal = Regex.decimal.withInverse(true);
Regex.alphanum = new CharClass(new CharRange('A', 'Z'), new CharRange('a', 'z'), new CharRange('0', '9'), '_');
Regex.nonAlphanum = Regex.alphanum.withInverse(true);
Regex.space = new CharClass(' ', '\f', '\n', '\r', '\t', '\v', String.fromCodePoint(0xa0), '\u1680', '\u180e', new CharRange('\u2000', '\u200a'), String.fromCodePoint(0x2028), '\u2029', '\u202f', '\u205f', '\u3000', '\ufeff');
Regex.nonSpace = Regex.space.withInverse(true);

// simple qualifiers
export class KleeneStar extends NumericQualifier { constructor() { super(); } }
export class KleenePlus extends NumericQualifier { constructor() { super({ min: 1 }); } }
export class Optional extends NumericQualifier { constructor() { super({ max: 1 }); } }


/* REGEX STATE MACHINE DEFINITION */

function charCodeToChar(_, charCode) {
    return String.fromCodePoint(parseInt(charCode, 16));
}

const regexStateMachine = new StateMachineEnvironment('regex')
    // top-level, where it all begins
    .withStateMachine('regex', new StateMachine('start')
        .withState('start', new State()
            .withTransition('^', { next: 'postStart', process: () => new Regex(new StartOfString()) })
            .withTransition('.', { next: 'normal', process: () => new Regex(new Wildcard()) })
            .withTransition('\\', { next: 'normal', stateMachine: 'escape', process: (_, char) => new Regex(char) })
            .withTransition('[', { next: 'normal', stateMachine: 'charClass', process: (_, cc) => new Regex(cc) })
            .withTransition('(', { next: 'normal', stateMachine: 'sequence', process: (_, cc) => new Regex(cc) })
            .withTransition('$*+?{|', { next: FAIL })
            .withDefaultTransition({ next: 'normal', process: (_, char) => new Regex(char) }))
        .withState('postStart', new State()
            .withTransition('^*+?{|', { next: FAIL })
            .withTransition('.', { next: 'normal', process: regex => regex.withElement(new Wildcard()) })
            .withTransition('\\', { next: 'normal', stateMachine: 'escape', process: (regex, char) => regex.withElement(char) })
            .withTransition('[', { next: 'normal', stateMachine: 'charClass', process: (regex, cc) => regex.withElement(cc) })
            .withTransition('(', { next: 'normal', stateMachine: 'sequence', process: (regex, seq) => regex.withElement(seq) })
            .withTransition('$', { next: DONE, process: regex => regex.withElement(new EndOfString()) })
            .withDefaultTransition({ next: 'normal', process: (regex, char) => regex.withElement(char) }))
        .withState('normal', new State({ isCompleteState: true }) // aside from DONE, this is the only state in the entire state machine that a processing can end on
            .withTransition('^|', { next: FAIL })
            .withTransition('.', { next: 'normal', process: regex => regex.withElement(new Wildcard()) })
            .withTransition('\\', { next: 'normal', stateMachine: 'escape', process: (regex, char) => regex.withElement(char) })
            .withTransition('[', { next: 'normal', stateMachine: 'charClass', process: (regex, cc) => regex.withElement(cc) })
            .withTransition('(', { next: 'normal', stateMachine: 'sequence', process: (regex, seq) => regex.withElement(seq) })
            .withTransition('$', { next: DONE, process: regex => regex.withElement(new EndOfString()) })
            .withTransition('*', { next: 'normal', process: regex => regex.wrapPrevious(new KleeneStar()) })
            .withTransition('+', { next: 'normal', process: regex => regex.wrapPrevious(new KleenePlus()) })
            .withTransition('?', { next: 'normal', process: regex => regex.wrapPrevious(new Optional()) })
            .withTransition('{', { next: 'normal', stateMachine: 'repetitionQualifier', process: (regex, qual) => regex.wrapPrevious(qual) })
            .withDefaultTransition({ next: 'normal', process: (regex, char) => regex.withElement(char) })))
    // sequence of elements surrounded by ()
    .withStateMachine('sequence', new StateMachine('start')
        .withState('start', new State()
            .withTransition('(', { next: 'seqStart', process: () => new Sequence() }))
        .withState('seqStart', new State()
            .withTransition('.', { next: 'normal', process: seq => seq.withElement(new Wildcard()) })
            .withTransition('\\', { next: 'normal', stateMachine: 'escape', process: (seq, char) => seq.withElement(char) })
            .withTransition('[', { next: 'normal', stateMachine: 'charClass', process: (seq, cc) => seq.withElement(cc) })
            .withTransition('(', { next: 'normal', stateMachine: 'sequence', process: (seq, seq2) => seq.withElement(seq2) })
            .withTransition('^$*+?{|)', { next: FAIL })
            .withDefaultTransition({ next: 'normal', process: (seq, char) => seq.withElement(char) }))
        .withState('normal', new State()
            .withTransition('.', { next: 'normal', process: seq => seq.withElement(new Wildcard()) })
            .withTransition('\\', { next: 'normal', stateMachine: 'escape', process: (seq, char) => seq.withElement(char) })
            .withTransition('[', { next: 'normal', stateMachine: 'charClass', process: (seq, cc) => seq.withElement(cc) })
            .withTransition('(', { next: 'normal', stateMachine: 'sequence', process: (seq, seq2) => seq.withElement(seq2) })
            .withTransition(')', { next: DONE, process: seq => seq.finish() })
            .withTransition('*', { next: 'normal', process: seq => seq.wrapPrevious(new KleeneStar()) })
            .withTransition('+', { next: 'normal', process: seq => seq.wrapPrevious(new KleenePlus()) })
            .withTransition('?', { next: 'normal', process: seq => seq.wrapPrevious(new Optional()) })
            .withTransition('{', { next: 'normal', stateMachine: 'repetitionQualifier', process: (seq, qual) => seq.wrapPrevious(qual) })
            .withTransition('|', { next: 'postEnum', process: seq => seq.startOrContinueEnumeration() })
            .withTransition('^$', { next: FAIL })
            .withDefaultTransition({ next: 'normal', process: (seq, char) => seq.withElement(char) }))
        .withState('postEnum', new State()
            .withTransition('.', { next: 'normal', process: seq => seq.withElement(new Wildcard()) })
            .withTransition('\\', { next: 'normal', stateMachine: 'escape', process: (seq, char) => seq.withElement(char) })
            .withTransition('[', { next: 'normal', stateMachine: 'charClass', process: (seq, cc) => seq.withElement(cc) })
            .withTransition('(', { next: 'normal', stateMachine: 'sequence', process: (seq, seq2) => seq.withElement(seq2) })
            .withTransition('^$|)*+?{', { next: FAIL })
            .withDefaultTransition({ next: 'normal', process: (seq, char) => seq.withElement(char) })))
    // {x,y} or {x,} or {x}
    .withStateMachine('repetitionQualifier', new StateMachine('start')
        .withState('start', new State()
            .withTransition('{', { next: 'qualStart', process: () => new NumericQualifier() }))
        .withState('qualStart', new State()
            .withRangedTransition('0', '9', { next: 'postStartNumber' }))
        .withState('postStartNumber', new State()
            .withRangedTransition('0', '9', { next: 'postStartNumber' })
            .withTransition(',', { next: 'possibleEndNumber', process: (qual, buffer) => qual.set('min', parseInt(buffer.substring(0, buffer.length - 1), 10)) })
            .withTransition('}', { next: DONE, process: (qual, buffer) => { const num = parseInt(buffer.substring(0, buffer.length - 1), 10); return qual.set('max', num).set('min', num); } }))
        .withState('possibleEndNumber', new State()
            .withTransition('}', { next: DONE, process: qual => qual })
            .withRangedTransition('0', '9', { next: 'endNumber' }))
        .withState('endNumber', new State()
            .withRangedTransition('0', '9', { next: 'endNumber' })
            .withTransition('}', { next: DONE, process: (qual, buffer) => qual.set('max', parseInt(buffer.substring(0, buffer.length - 1), 10)) })))
    // character classes, e.g. [abc0-9] or [^abc]
    .withStateMachine('charClass', new StateMachine('start')
        .withState('start', new State()
            .withTransition('[', { next: 'ccStart', process: () => new CharClass() }))
        .withState('ccStart', new State()
            .withTransition('^', { next: 'ccPostInverseStart', process: cc => cc.withInverse(true) }) // ^ inverts the character class
            .withTransition('-]', { next: FAIL }) // - must be preceded by a character, and ] must be preceded by something
            .withTransition('\\', { next: 'ccPossibleRange', stateMachine: 'escape', process: (cc, char) => cc.withPossibleRange(char) })
            .withDefaultTransition({ next: 'ccPossibleRange', process: (cc, char) => cc.withPossibleRange(char) }))
        .withState('ccPostInverseStart', new State()
            .withTransition('^-]', { next: FAIL })
            .withTransition('\\', { next: 'ccPossibleRange', stateMachine: 'escape', process: (cc, char) => cc.withPossibleRange(char) })
            .withDefaultTransition({ next: 'ccPossibleRange', process: (cc, char) => cc.withPossibleRange(char) }))
        .withState('ccNewChar', new State()
            .withTransition('^-', { next: FAIL }) // we can't see ^ after the start, and - has to be preceded by a character not in a range
            .withTransition(']', { next: DONE, process: cc => cc.finish() }) // ] closes the class, we're done
            .withTransition('\\', { next: 'ccPossibleRange', stateMachine: 'escape', process: (cc, char) => cc.withPossibleRange(char) })
            .withDefaultTransition({ next: 'ccPossibleRange', process: (cc, char) => cc.withPossibleRange(char) }))
        .withState('ccPossibleRange', new State()
            .withTransition('-', { next: 'ccRange', flush: true }) // definitely a range
            .withTransition('^', { next: FAIL })
            .withTransition(']', { next: DONE, process: cc => cc.finish() })
            .withTransition('\\', { next: 'ccPossibleRange', stateMachine: 'escape', process: (cc, char) => cc.withPossibleRange(char) })
            .withDefaultTransition({ next: 'ccPossibleRange', process: (cc, char) => cc.withPossibleRange(char) }))
        .withState('ccRange', new State()
            .withTransition('^-]', { next: FAIL })
            .withTransition('\\', { next: 'ccNewChar', stateMachine: 'escape', process: (cc, char) => cc.withRange(char) })
            .withDefaultTransition({ next: 'ccNewChar', process: (cc, char) => cc.withRange(char) })))
    // escape sequences, starting with a backslash
    .withStateMachine('escape', new StateMachine('start')
        .withState('start', new State()
            .withTransition('\\', { next: 'escape', flush: true }))
        .withState('escape', new State()
            // built-in character classes
            .withTransition('d', { next: DONE, process: () => Regex.decimal })
            .withTransition('D', { next: DONE, process: () => Regex.nonDecimal })
            .withTransition('w', { next: DONE, process: () => Regex.alphanum })
            .withTransition('W', { next: DONE, process: () => Regex.nonAlphanum })
            .withTransition('s', { next: DONE, process: () => Regex.space })
            .withTransition('S', { next: DONE, process: () => Regex.nonSpace })
            // normal escape characters
            .withTransition('v', { next: DONE, process: () => '\v' }) // JSON.parse doesn't work for \v
            .withTransition('trnfb', { next: DONE, process: (_, char) => JSON.parse(`"\\${char}"`) })
            // character code sequences
            .withTransition('x', { next: 'escapeCodeHex', flush: true })
            .withTransition('u', { next: 'escapeCodeUni', flush: true })
            // unescape sequences, any other character has no special escape behavior, so it should just resolve to the character
            .withDefaultTransition({ next: DONE, process: (_, char) => char }))
        // \xhh
        .withState('escapeCodeHex', new State()
            .withRangedTransition('0', '9', { next: 'escapeCodeHex2' })
            .withRangedTransition('a', 'f', { next: 'escapeCodeHex2' })
            .withRangedTransition('A', 'F', { next: 'escapeCodeHex2' }))
        .withState('escapeCodeHex2', new State()
            .withRangedTransition('0', '9', { next: DONE, process: charCodeToChar })
            .withRangedTransition('a', 'f', { next: DONE, process: charCodeToChar })
            .withRangedTransition('A', 'F', { next: DONE, process: charCodeToChar }))
        // \uhhhh
        .withState('escapeCodeUni', new State()
            .withTransition('{', { next: 'escapeCodeUniExplicit', flush: true })
            .withRangedTransition('0', '9', { next: 'escapeCodeUni2' })
            .withRangedTransition('a', 'f', { next: 'escapeCodeUni2' })
            .withRangedTransition('A', 'F', { next: 'escapeCodeUni2' }))
        .withState('escapeCodeUni2', new State()
            .withRangedTransition('0', '9', { next: 'escapeCodeUni3' })
            .withRangedTransition('a', 'f', { next: 'escapeCodeUni3' })
            .withRangedTransition('A', 'F', { next: 'escapeCodeUni3' }))
        .withState('escapeCodeUni3', new State()
            .withRangedTransition('0', '9', { next: 'escapeCodeUni4' })
            .withRangedTransition('a', 'f', { next: 'escapeCodeUni4' })
            .withRangedTransition('A', 'F', { next: 'escapeCodeUni4' }))
        .withState('escapeCodeUni4', new State()
            .withRangedTransition('0', '9', { next: DONE, process: charCodeToChar })
            .withRangedTransition('a', 'f', { next: DONE, process: charCodeToChar })
            .withRangedTransition('A', 'F', { next: DONE, process: charCodeToChar }))
        // \u{hhhh} | \u{hhhhh}
        .withState('escapeCodeUniExplicit', new State()
            .withRangedTransition('0', '9', { next: 'escapeCodeUniExplicit2' })
            .withRangedTransition('a', 'f', { next: 'escapeCodeUniExplicit2' })
            .withRangedTransition('A', 'F', { next: 'escapeCodeUniExplicit2' }))
        .withState('escapeCodeUniExplicit2', new State()
            .withRangedTransition('0', '9', { next: 'escapeCodeUniExplicit3' })
            .withRangedTransition('a', 'f', { next: 'escapeCodeUniExplicit3' })
            .withRangedTransition('A', 'F', { next: 'escapeCodeUniExplicit3' }))
        .withState('escapeCodeUniExplicit3', new State()
            .withRangedTransition('0', '9', { next: 'escapeCodeUniExplicit4' })
            .withRangedTransition('a', 'f', { next: 'escapeCodeUniExplicit4' })
            .withRangedTransition('A', 'F', { next: 'escapeCodeUniExplicit4' }))
        .withState('escapeCodeUniExplicit4', new State()
            .withRangedTransition('0', '9', { next: 'escapeCodeUniExplicit5' })
            .withRangedTransition('a', 'f', { next: 'escapeCodeUniExplicit5' })
            .withRangedTransition('A', 'F', { next: 'escapeCodeUniExplicit5' }))
        .withState('escapeCodeUniExplicit5', new State()
            .withTransition('}', { next: DONE, process: (_, buffer) => charCodeToChar(_, buffer.substring(0, buffer.length - 1)) })
            .withRangedTransition('0', '9', { next: 'escapeCodeUniExplicit6' })
            .withRangedTransition('a', 'f', { next: 'escapeCodeUniExplicit6' })
            .withRangedTransition('A', 'F', { next: 'escapeCodeUniExplicit6' }))
        .withState('escapeCodeUniExplicit6', new State()
            .withTransition('}', { next: DONE, process: (_, buffer) => charCodeToChar(_, buffer.substring(0, buffer.length - 1)) })));
