const util = require('./util')

module.exports = function stringify (value, replacer, space) {

const stack = []
let indent = ''
let propertyList
let replacerFunc
let gap = ''
let quote

if (
    replacer != null &&
    typeof replacer === 'object' &&
    !Array.isArray(replacer)
) {
    space = replacer.space
    quote = replacer.quote
    replacer = replacer.replacer
}

if (typeof replacer === 'function') {
    replacerFunc = replacer
} else if (Array.isArray(replacer)) {
    propertyList = []
    for (const v of replacer) {
        let item

        if (typeof v === 'string') {
            item = v
        } else if (
            typeof v === 'number' ||
            v instanceof String ||
            v instanceof Number
        ) {
            item = String(v)
        }

        if (item !== undefined && propertyList.indexOf(item) < 0) {
            propertyList.push(item)
        }
    }
}

if (space instanceof Number) {
    space = Number(space)
} else if (space instanceof String) {
    space = String(space)
}

if (typeof space === 'number') {
    if (space > 0) {
        space = Math.min(10, Math.floor(space))
        gap = '          '.substr(0, space)
    }
} else if (typeof space === 'string') {
    gap = space.substr(0, 10)
}

return serializeProperty('', {'': value})

function serializeProperty (key, holder) {
    let value = holder[key]
    if (value != null) {
        if (typeof value.toJSON5 === 'function') {
            value = value.toJSON5(key)
        } else if (typeof value.toJSON === 'function') {
            value = value.toJSON(key)
        }
    }

    if (replacerFunc) {
        value = replacerFunc.call(holder, key, value)
    }

    if (value instanceof Number) {
        value = Number(value)
    } else if (value instanceof String) {
        value = String(value)
    } else if (value instanceof Boolean) {
        value = value.valueOf()
    }

    switch (value) {
    case null: return 'null'
    case true: return 'true'
    case false: return 'false'
    }

    if (typeof value === 'string') {
        return quoteString(value, false)
    }

    if (typeof value === 'number') {
        return String(value)
    }

    if (typeof value === 'object') {
        return Array.isArray(value) ? serializeArray(value) : serializeObject(value)
    }

    return undefined
}

function quoteString (value) {
    const quotes = {
        "'": 0.1,
        '"': 0.2,
    }

    const replacements = {
        "'": "\\'",
        '"': '\\"',
        '\\': '\\\\',
        '\b': '\\b',
        '\f': '\\f',
        '\n': '\\n',
        '\r': '\\r',
        '\t': '\\t',
        '\v': '\\v',
        '\0': '\\0',
        '\u2028': '\\u2028',
        '\u2029': '\\u2029',
    }

    let product = ''

    for (let i = 0; i < value.length; i++) {
        const c = value[i]
        switch (c) {
        case "'":
        case '"':
            quotes[c]++
            product += c
            continue

        case '\0':
            if (util.isDigit(value[i + 1])) {
                product += '\\x00'
                continue
            }
        }

        if (replacements[c]) {
            product += replacements[c]
            continue
        }

        if (c < ' ') {
            let hexString = c.charCodeAt(0).toString(16)
            product += '\\x' + ('00' + hexString).substring(hexString.length)
            continue
        }

        product += c
    }

    const quoteChar = quote || Object.keys(quotes).reduce((a, b) => (quotes[a] < quotes[b]) ? a : b)

    product = product.replace(new RegExp(quoteChar, 'g'), replacements[quoteChar])

    return quoteChar + product + quoteChar
}

function serializeObject (value) {
    if (stack.indexOf(value) >= 0) {
        throw TypeError('Converting circular structure to JSON5')
    }

    stack.push(value)

    let stepback = indent
    indent = indent + gap

    let keys = propertyList || Object.keys(value)
    let partial = []
    for (const key of keys) {
        const propertyString = serializeProperty(key, value)
        if (propertyString !== undefined) {
            let member = serializeKey(key) + ':'
            if (gap !== '') {
                member += ' '
            }
            member += propertyString
            partial.push(member)
        }
    }

    let final
    if (partial.length === 0) {
        final = '{}'
    } else {
        let properties
        if (gap === '') {
            properties = partial.join(',')
            final = '{' + properties + '}'
        } else {
            let separator = ',\n' + indent
            properties = partial.join(separator)
            final = '{\n' + indent + properties + ',\n' + stepback + '}'
        }
    }

    stack.pop()
    indent = stepback
    return final
}

function serializeKey (key) {
    if (key.length === 0) {
        return quoteString(key, true)
    }

    const firstChar = String.fromCodePoint(key.codePointAt(0))
    if (!util.isIdStartChar(firstChar)) {
        return quoteString(key, true)
    }

    for (let i = firstChar.length; i < key.length; i++) {
        if (!util.isIdContinueChar(String.fromCodePoint(key.codePointAt(i)))) {
            return quoteString(key, true)
        }
    }

    return key
}

function serializeArray (value) {
    if (stack.indexOf(value) >= 0) {
        throw TypeError('Converting circular structure to JSON5')
    }

    stack.push(value)

    let stepback = indent
    indent = indent + gap

    let partial = []
    for (let i = 0; i < value.length; i++) {
        const propertyString = serializeProperty(String(i), value)
        partial.push((propertyString !== undefined) ? propertyString : 'null')
    }

    let final
    if (partial.length === 0) {
        final = '[]'
    } else {
        if (gap === '') {
            let properties = partial.join(',')
            final = '[' + properties + ']'
        } else {
            let separator = ',\n' + indent
            let properties = partial.join(separator)
            final = '[\n' + indent + properties + ',\n' + stepback + ']'
        }
    }

    stack.pop()
    indent = stepback
    return final
}

}