
Pattern         = require './Pattern'
Unescaper       = require './Unescaper'
Escaper         = require './Escaper'
Utils           = require './Utils'
ParseException  = require './Exception/ParseException'
DumpException   = require './Exception/DumpException'

# Inline YAML parsing and dumping
class Inline

    # Quoted string regular expression
    @REGEX_QUOTED_STRING:               '(?:"(?:[^"\\\\]*(?:\\\\.[^"\\\\]*)*)"|\'(?:[^\']*(?:\'\'[^\']*)*)\')'

    # Pre-compiled patterns
    #
    @PATTERN_TRAILING_COMMENTS:         new Pattern '^\\s*#.*$'
    @PATTERN_QUOTED_SCALAR:             new Pattern '^'+@REGEX_QUOTED_STRING
    @PATTERN_THOUSAND_NUMERIC_SCALAR:   new Pattern '^(-|\\+)?[0-9,]+(\\.[0-9]+)?$'
    @PATTERN_SCALAR_BY_DELIMITERS:      {}

    # Settings
    @settings: {}


    # Configure YAML inline.
    #
    # @param [Boolean]  exceptionOnInvalidType  true if an exception must be thrown on invalid types (a JavaScript resource or object), false otherwise
    # @param [Function] objectDecoder           A function to deserialize custom objects, null otherwise
    #
    @configure: (exceptionOnInvalidType = null, objectDecoder = null) ->
        # Update settings
        @settings.exceptionOnInvalidType = exceptionOnInvalidType
        @settings.objectDecoder = objectDecoder
        return


    # Converts a YAML string to a JavaScript object.
    #
    # @param [String]   value                   A YAML string
    # @param [Boolean]  exceptionOnInvalidType  true if an exception must be thrown on invalid types (a JavaScript resource or object), false otherwise
    # @param [Function] objectDecoder           A function to deserialize custom objects, null otherwise
    #
    # @return [Object]  A JavaScript object representing the YAML string
    #
    # @throw [ParseException]
    #
    @parse: (value, exceptionOnInvalidType = false, objectDecoder = null) ->
        # Update settings from last call of Inline.parse()
        @settings.exceptionOnInvalidType = exceptionOnInvalidType
        @settings.objectDecoder = objectDecoder

        if not value?
            return ''

        value = Utils.trim value

        if 0 is value.length
            return ''

        # Keep a context object to pass through static methods
        context = {exceptionOnInvalidType, objectDecoder, i: 0}

        switch value.charAt(0)
            when '['
                result = @parseSequence value, context
                ++context.i
            when '{'
                result = @parseMapping value, context
                ++context.i
            else
                result = @parseScalar value, null, ['"', "'"], context

        # Some comments are allowed at the end
        if @PATTERN_TRAILING_COMMENTS.replace(value[context.i..], '') isnt ''
            throw new ParseException 'Unexpected characters near "'+value[context.i..]+'".'

        return result


    # Dumps a given JavaScript variable to a YAML string.
    #
    # @param [Object]   value                   The JavaScript variable to convert
    # @param [Boolean]  exceptionOnInvalidType  true if an exception must be thrown on invalid types (a JavaScript resource or object), false otherwise
    # @param [Function] objectEncoder           A function to serialize custom objects, null otherwise
    #
    # @return [String]  The YAML string representing the JavaScript object
    #
    # @throw [DumpException]
    #
    @dump: (value, exceptionOnInvalidType = false, objectEncoder = null) ->
        if not value?
            return 'null'
        type = typeof value
        if type is 'object'
            if value instanceof Date
                return value.toISOString()
            else if objectEncoder?
                result = objectEncoder value
                if typeof result is 'string' or result?
                    return result
            return @dumpObject value
        if type is 'boolean'
            return (if value then 'true' else 'false')
        if Utils.isDigits(value)
            return (if type is 'string' then "'"+value+"'" else String(parseInt(value)))
        if Utils.isNumeric(value)
            return (if type is 'string' then "'"+value+"'" else String(parseFloat(value)))
        if type is 'number'
            return (if value is Infinity then '.Inf' else (if value is -Infinity then '-.Inf' else (if isNaN(value) then '.NaN' else value)))
        if Escaper.requiresDoubleQuoting value
            return Escaper.escapeWithDoubleQuotes value
        if Escaper.requiresSingleQuoting value
            return Escaper.escapeWithSingleQuotes value
        if '' is value
            return '""'
        if Utils.PATTERN_DATE.test value
            return "'"+value+"'";
        if value.toLowerCase() in ['null','~','true','false']
            return "'"+value+"'"
        # Default
        return value;


    # Dumps a JavaScript object to a YAML string.
    #
    # @param [Object]   value                   The JavaScript object to dump
    # @param [Boolean]  exceptionOnInvalidType  true if an exception must be thrown on invalid types (a JavaScript resource or object), false otherwise
    # @param [Function] objectEncoder           A function do serialize custom objects, null otherwise
    #
    # @return string The YAML string representing the JavaScript object
    #
    @dumpObject: (value, exceptionOnInvalidType, objectSupport = null) ->
        # Array
        if value instanceof Array
            output = []
            for val in value
                output.push @dump val
            return '['+output.join(', ')+']'

        # Mapping
        else
            output = []
            for key, val of value
                output.push @dump(key)+': '+@dump(val)
            return '{'+output.join(', ')+'}'


    # Parses a scalar to a YAML string.
    #
    # @param [Object]   scalar
    # @param [Array]    delimiters
    # @param [Array]    stringDelimiters
    # @param [Object]   context
    # @param [Boolean]  evaluate
    #
    # @return [String]  A YAML string
    #
    # @throw [ParseException] When malformed inline YAML string is parsed
    #
    @parseScalar: (scalar, delimiters = null, stringDelimiters = ['"', "'"], context = null, evaluate = true) ->
        unless context?
            context = exceptionOnInvalidType: @settings.exceptionOnInvalidType, objectDecoder: @settings.objectDecoder, i: 0
        {i} = context

        if scalar.charAt(i) in stringDelimiters
            # Quoted scalar
            output = @parseQuotedScalar scalar, context
            {i} = context

            if delimiters?
                tmp = Utils.ltrim scalar[i..], ' '
                if not(tmp.charAt(0) in delimiters)
                    throw new ParseException 'Unexpected characters ('+scalar[i..]+').'

        else
            # "normal" string
            if not delimiters
                output = scalar[i..]
                i += output.length

                # Remove comments
                strpos = output.indexOf ' #'
                if strpos isnt -1
                    output = Utils.rtrim output[0...strpos]

            else
                joinedDelimiters = delimiters.join('|')
                pattern = @PATTERN_SCALAR_BY_DELIMITERS[joinedDelimiters]
                unless pattern?
                    pattern = new Pattern '^(.+?)('+joinedDelimiters+')'
                    @PATTERN_SCALAR_BY_DELIMITERS[joinedDelimiters] = pattern
                if match = pattern.exec scalar[i..]
                    output = match[1]
                    i += output.length
                else
                    throw new ParseException 'Malformed inline YAML string ('+scalar+').'


            if evaluate
                output = @evaluateScalar output, context

        context.i = i
        return output


    # Parses a quoted scalar to YAML.
    #
    # @param [String]   scalar
    # @param [Object]   context
    #
    # @return [String]  A YAML string
    #
    # @throw [ParseException] When malformed inline YAML string is parsed
    #
    @parseQuotedScalar: (scalar, context) ->
        {i} = context

        unless match = @PATTERN_QUOTED_SCALAR.exec scalar[i..]
            throw new ParseException 'Malformed inline YAML string ('+scalar[i..]+').'

        output = match[0].substr(1, match[0].length - 2)

        if '"' is scalar.charAt(i)
            output = Unescaper.unescapeDoubleQuotedString output
        else
            output = Unescaper.unescapeSingleQuotedString output

        i += match[0].length

        context.i = i
        return output


    # Parses a sequence to a YAML string.
    #
    # @param [String]   sequence
    # @param [Object]   context
    #
    # @return [String]  A YAML string
    #
    # @throw [ParseException] When malformed inline YAML string is parsed
    #
    @parseSequence: (sequence, context) ->
        output = []
        len = sequence.length
        {i} = context
        i += 1

        # [foo, bar, ...]
        while i < len
            context.i = i
            switch sequence.charAt(i)
                when '['
                    # Nested sequence
                    output.push @parseSequence sequence, context
                    {i} = context
                when '{'
                    # Nested mapping
                    output.push @parseMapping sequence, context
                    {i} = context
                when ']'
                    return output
                when ',', ' ', "\n"
                    # Do nothing
                else
                    isQuoted = (sequence.charAt(i) in ['"', "'"])
                    value = @parseScalar sequence, [',', ']'], ['"', "'"], context
                    {i} = context

                    if not(isQuoted) and typeof(value) is 'string' and (value.indexOf(': ') isnt -1 or value.indexOf(":\n") isnt -1)
                        # Embedded mapping?
                        try
                            value = @parseMapping '{'+value+'}'
                        catch e
                            # No, it's not


                    output.push value

                    --i

            ++i

        throw new ParseException 'Malformed inline YAML string '+sequence


    # Parses a mapping to a YAML string.
    #
    # @param [String]   mapping
    # @param [Object]   context
    #
    # @return [String]  A YAML string
    #
    # @throw [ParseException] When malformed inline YAML string is parsed
    #
    @parseMapping: (mapping, context) ->
        output = {}
        len = mapping.length
        {i} = context
        i += 1

        # {foo: bar, bar:foo, ...}
        shouldContinueWhileLoop = false
        while i < len
            context.i = i
            switch mapping.charAt(i)
                when ' ', ',', "\n"
                    ++i
                    context.i = i
                    shouldContinueWhileLoop = true
                when '}'
                    return output

            if shouldContinueWhileLoop
                shouldContinueWhileLoop = false
                continue

            # Key
            key = @parseScalar mapping, [':', ' ', "\n"], ['"', "'"], context, false
            {i} = context

            # Value
            done = false

            while i < len
                context.i = i
                switch mapping.charAt(i)
                    when '['
                        # Nested sequence
                        value = @parseSequence mapping, context
                        {i} = context
                        # Spec: Keys MUST be unique; first one wins.
                        # Parser cannot abort this mapping earlier, since lines
                        # are processed sequentially.
                        if output[key] == undefined
                            output[key] = value
                        done = true
                    when '{'
                        # Nested mapping
                        value = @parseMapping mapping, context
                        {i} = context
                        # Spec: Keys MUST be unique; first one wins.
                        # Parser cannot abort this mapping earlier, since lines
                        # are processed sequentially.
                        if output[key] == undefined
                            output[key] = value
                        done = true
                    when ':', ' ', "\n"
                        # Do nothing
                    else
                        value = @parseScalar mapping, [',', '}'], ['"', "'"], context
                        {i} = context
                        # Spec: Keys MUST be unique; first one wins.
                        # Parser cannot abort this mapping earlier, since lines
                        # are processed sequentially.
                        if output[key] == undefined
                            output[key] = value
                        done = true
                        --i

                ++i

                if done
                    break

        throw new ParseException 'Malformed inline YAML string '+mapping


    # Evaluates scalars and replaces magic values.
    #
    # @param [String]   scalar
    #
    # @return [String]  A YAML string
    #
    @evaluateScalar: (scalar, context) ->
        scalar = Utils.trim(scalar)
        scalarLower = scalar.toLowerCase()

        switch scalarLower
            when 'null', '', '~'
                return null
            when 'true'
                return true
            when 'false'
                return false
            when '.inf'
                return Infinity
            when '.nan'
                return NaN
            when '-.inf'
                return Infinity
            else
                firstChar = scalarLower.charAt(0)
                switch firstChar
                    when '!'
                        firstSpace = scalar.indexOf(' ')
                        if firstSpace is -1
                            firstWord = scalarLower
                        else
                            firstWord = scalarLower[0...firstSpace]
                        switch firstWord
                            when '!'
                                if firstSpace isnt -1
                                    return parseInt @parseScalar(scalar[2..])
                                return null
                            when '!str'
                                return Utils.ltrim scalar[4..]
                            when '!!str'
                                return Utils.ltrim scalar[5..]
                            when '!!int'
                                return parseInt(@parseScalar(scalar[5..]))
                            when '!!bool'
                                return Utils.parseBoolean(@parseScalar(scalar[6..]), false)
                            when '!!float'
                                return parseFloat(@parseScalar(scalar[7..]))
                            when '!!timestamp'
                                return Utils.stringToDate(Utils.ltrim(scalar[11..]))
                            else
                                unless context?
                                    context = exceptionOnInvalidType: @settings.exceptionOnInvalidType, objectDecoder: @settings.objectDecoder, i: 0
                                {objectDecoder, exceptionOnInvalidType} = context

                                if objectDecoder
                                    # If objectDecoder function is given, we can do custom decoding of custom types
                                    trimmedScalar = Utils.rtrim scalar
                                    firstSpace = trimmedScalar.indexOf(' ')
                                    if firstSpace is -1
                                        return objectDecoder trimmedScalar, null
                                    else
                                        subValue = Utils.ltrim trimmedScalar[firstSpace+1..]
                                        unless subValue.length > 0
                                            subValue = null
                                        return objectDecoder trimmedScalar[0...firstSpace], subValue

                                if exceptionOnInvalidType
                                    throw new ParseException 'Custom object support when parsing a YAML file has been disabled.'

                                return null
                    when '0'
                        if '0x' is scalar[0...2]
                            return Utils.hexDec scalar
                        else if Utils.isDigits scalar
                            return Utils.octDec scalar
                        else if Utils.isNumeric scalar
                            return parseFloat scalar
                        else
                            return scalar
                    when '+'
                        if Utils.isDigits scalar
                            raw = scalar
                            cast = parseInt(raw)
                            if raw is String(cast)
                                return cast
                            else
                                return raw
                        else if Utils.isNumeric scalar
                            return parseFloat scalar
                        else if @PATTERN_THOUSAND_NUMERIC_SCALAR.test scalar
                            return parseFloat(scalar.replace(',', ''))
                        return scalar
                    when '-'
                        if Utils.isDigits(scalar[1..])
                            if '0' is scalar.charAt(1)
                                return -Utils.octDec(scalar[1..])
                            else
                                raw = scalar[1..]
                                cast = parseInt(raw)
                                if raw is String(cast)
                                    return -cast
                                else
                                    return -raw
                        else if Utils.isNumeric scalar
                            return parseFloat scalar
                        else if @PATTERN_THOUSAND_NUMERIC_SCALAR.test scalar
                            return parseFloat(scalar.replace(',', ''))
                        return scalar
                    else
                        if date = Utils.stringToDate(scalar)
                            return date
                        else if Utils.isNumeric(scalar)
                            return parseFloat scalar
                        else if @PATTERN_THOUSAND_NUMERIC_SCALAR.test scalar
                            return parseFloat(scalar.replace(',', ''))
                        return scalar

module.exports = Inline
