import type { Maybe } from "Maybe"

import List from "List"



#iftarget js

import { Just, Nothing } from "Maybe"



#endif


#iftarget js

/**
 * Lowercase a string
 *
 * @since 0.0.5
 * @example
 * toLower("COOL") // "cool"
 */
toLower :: String -> String
export toLower = (s) => #- { return s.toLowerCase() } -#

/**
 * Uppercase a string
 *
 * @since 0.0.5
 * @example
 * toUpper("nice") // "NICE"
 */
toUpper :: String -> String
export toUpper = (s) => #- { return s.toUpperCase() } -#

#elseif llvm

/**
 * Lowercase a string
 *
 * @since 0.0.5
 * @example
 * toLower("COOL") // "cool"
 */
toLower :: String -> String
export toLower = extern "madlib__string__toLower"

/**
 * Uppercase a string
 *
 * @since 0.0.5
 * @example
 * toUpper("nice") // "NICE"
 */
toUpper :: String -> String
export toUpper = extern "madlib__string__toUpper"

#endif

#iftarget js

/**
 * Split a string given another string
 *
 * @since 0.0.5
 * @example
 * split("x", `axbxcxd`) // ["a", "b", "c", "d"]
 */
split :: String -> String -> List String
export split = (separator, str) => #- {
  const items = str.split(separator)

  if (items.length === 0) {
    return null
  }

  let current = {}
  let output = current
  items.forEach((item) => {
    current = current.n = {}
    current.v = item
  })
  current.n = null

  return output.n
} -#

#elseif llvm

/**
 * Split a string given another string
 *
 * @since 0.0.5
 * @example
 * split("x", `axbxcxd`) // ["a", "b", "c", "d"]
 */
split :: String -> String -> List String
export split = extern "madlib__string__split"

#endif


/**
 * Joins a list with a separator and returns a String.
 *
 * IMPORTANT:
 * When coming from JS, beware that there is a small difference with Array.prototype.join.
 * In JS, join relies on its dynamic nature and accepts any type as being valid, and transforms it
 * to a string for you. In Madlib you need to provide an instance of Show for your custom types, and
 * for Madlib types (eg. tuples, List, Boolean, Maybe) it uses the Show instance defined for them.
 *
 * @since 0.0.5
 * @example
 * join(" and ", ["cats", "dogs"])   // "cats and dogs"
 * join("", ["one", "two", "three"]) // "onetwothree"
 */
#iftarget js
join :: String -> List String -> String
export join = (a, xs) => pipe(
  List.intersperse(a),
  List.reduce(mappend, ""),
)(xs)

#elseif llvm
join :: String -> List String -> String
export join = extern "madlib__string__join"
#endif

/**
 * Split a string by newlines
 *
 * @since 0.0.5
 * @example
 * lines(`line1
 * line2
 * line3`) // ["line1", "line2", "line3"]
 */
lines :: String -> List String
export lines = split("\n")

/**
 * Join a list of strings by newlines
 *
 * @since 0.0.5
 * @example
 * unlines(["line1", "line2", "line3"]) // "line1\nline2\nline3"
 */
unlines :: List String -> String
export unlines = join("\n")

/**
 * Split a string by spaces
 * @since 0.18.7
 */
words :: String -> List String
export words = split(" ")

/**
 * Join a list of strings by spaces
 * @since 0.18.7
 */
unwords :: List String -> String
export unwords = join(" ")


#iftarget js

/**
 * Convert a string into a list of characters
 * @since 0.18.7
 */
toList :: String -> List Char
export toList = (str) => #- {
  if (str.length === 0) {
    return null
  }

  let result = { v: null, n: null }
  let current = result
  str.split('').forEach(c => {
    current = current.n = { v: c, n: null }
  })
  return result.n
} -#

/**
 * Convert a list of characters to a string
 * @since 0.18.7
 */
fromList :: List Char -> String
export fromList = (list) => #- {
  let chars = []
  while (list !== null) {
    chars.push(list.v)
    list = list.n
  }
  return chars.join('')
} -#

#elseif llvm

/**
 * Convert a string into a list of characters
 * @since 0.18.7
 */
toList :: String -> List Char
export toList = extern "madlib__string__toList"

/**
 * Convert a list of characters to a string
 * @since 0.18.7
 */
fromList :: List Char -> String
export fromList = extern "madlib__string__fromList"

#endif


singleton :: Char -> String
export singleton = (c) => fromList([c])


#iftarget js

mapChars :: (Char -> Char) -> String -> String
export mapChars = (f, s) => #- s.split("").map(f).join("") -#

#elseif llvm

/**
 * `map` the characters in a string
 * @since 0.12.0
 */
mapChars :: (Char -> Char) -> String -> String
export mapChars = extern "madlib__string__mapChars"

#endif

/**
 * `filter` the characters in a string
 * @since 0.18.7
 */
filterChars :: (Char -> Boolean) -> String -> String
export filterChars = (predicate, s) => pipe(
  toList,
  List.filter(predicate),
  fromList,
)(s)

/**
 * `reduce` over a list of characters
 * @since 0.18.7
 */
reduceChars :: (a -> Char -> a) -> a -> String -> a
export reduceChars = (f, initial, s) => pipe(
  toList,
  List.reduce(f, initial),
)(s)


#iftarget js

/**
 * Cut a segment from a string
 * @since 0.18.7
 */
slice :: Integer -> Integer -> String -> String
export slice = (start, end, s) => #- { return s.slice(start, end === 0 ? s.length : end) } -#

#elseif llvm

/**
 * Cut a segment from string
 * @since 0.18.7
 */
slice :: Integer -> Integer -> String -> String
export slice = extern "madlib__string__slice"

#endif

/**
 * Test whether a string is empty / zero-length
 * @since 0.18.7
 */
isEmpty :: String -> Boolean
export isEmpty = (s) => s == ""

/**
 * Drop characters at the beginning of the string
 * @since 0.18.7
 */
drop :: Integer -> String -> String
export drop = (n, s) => slice(n, 0, s)

/**
 * Drop characters at the end of the string
 * @since 0.18.7
 */
dropLast :: Integer -> String -> String
export dropLast = (n, s) => slice(0, -n, s)

/**
 * Drop characters which match a predicate function
 * @since 0.18.7
 */
dropWhile :: (Char -> Boolean) -> String -> String
export dropWhile = (predicate, s) => pipe(
  toList,
  List.dropWhile(predicate),
  fromList,
)(s)

/**
 * Take characters from the beginning of the string
 * @since 0.18.7
 */
take :: Integer -> String -> String
export take = (n, s) => slice(0, n, s)

/**
 * Take characters from the end of the string
 * @since 0.18.7
 */
takeLast :: Integer -> String -> String
export takeLast = (n, s) => slice(-n, 0, s)

/**
 * Take characters which match a predicate function
 * @since 0.18.7
 */
takeWhile :: (Char -> Boolean) -> String -> String
export takeWhile = (predicate, s) => pipe(
  toList,
  List.takeWhile(predicate),
  fromList,
)(s)


#iftarget js

_charAt :: Maybe Char -> (Char -> Maybe Char) -> Integer -> String -> Maybe Char
_charAt = (nothing, just, n, s) => #- {
  const c = s[n]
  return !!c ? just(c) : nothing
} -#

charAt :: Integer -> String -> Maybe Char
export charAt = (n, s) => _charAt(Nothing, Just, n, s)

#elseif llvm

/**
 * Return the character at a given index
 * @since 0.18.7
 */
charAt :: Integer -> String -> Maybe Char
export charAt = extern "madlib__string__charAt"

#endif

// ---------------------------------------------------------------------------
// Byte-offset-based access — used internally by the Parse module for O(N) parsing
// ---------------------------------------------------------------------------

#iftarget js

// On JS, "byte offset" means UTF-16 code-unit offset (what JS strings use natively).

byteCharAt :: Integer -> String -> Maybe Char
export byteCharAt = (offset, s) => #- (() => {
  if (offset >= s.length) return {__constructor: "Nothing", __a: 0}
  const code = s.charCodeAt(offset)
  // High surrogate: decode the full codepoint from the pair
  if (code >= 0xD800 && code <= 0xDBFF) {
    const low = s.charCodeAt(offset + 1)
    return {__constructor: "Just", __a: 1, _0: String.fromCodePoint(((code - 0xD800) << 10) + (low - 0xDC00) + 0x10000)}
  }
  return {__constructor: "Just", __a: 1, _0: s[offset]}
})() -#

byteCharWidth :: Integer -> String -> Integer
export byteCharWidth = (offset, s) => #- (() => {
  const code = s.charCodeAt(offset)
  return (code >= 0xD800 && code <= 0xDBFF) ? 2 : 1
})() -#

byteLength :: String -> Integer
export byteLength = (s) => #- s.length -#

byteStartsWith :: String -> Integer -> String -> Boolean
export byteStartsWith = (target, offset, s) => #- s.startsWith(target, offset) -#

#elseif llvm

byteCharAt :: Integer -> String -> Maybe Char
export byteCharAt = extern "madlib__string__byteCharAt"

byteCharWidth :: Integer -> String -> Integer
export byteCharWidth = extern "madlib__string__byteCharWidth"

byteLength :: String -> Integer
export byteLength = extern "madlib__string__byteLength"

byteStartsWith :: String -> Integer -> String -> Boolean
export byteStartsWith = extern "madlib__string__byteStartsWith"

#endif

// ---------------------------------------------------------------------------

/**
 * Return the first character in a string
 * @since 0.18.7
 */
firstChar :: String -> Maybe Char
export firstChar = charAt(0)

/**
 * Return the last character in a string
 * @since 0.18.7
 */
lastChar :: String -> Maybe Char
export lastChar = (s) => charAt(length(s) - 1, s)

#iftarget js

/**
 * Trim the leading and trailing whitespace
 * @since 0.18.7
 */
trim :: String -> String
export trim = (s) => #- { return s.trim() } -#

/**
 * Trim the leading whitespace
 * @since 0.18.7
 */
trimStart :: String -> String
export trimStart = (s) => #- { return s.trimStart() } -#

/**
 * Trim the trailing whitespace
 * @since 0.18.7
 */
trimEnd :: String -> String
export trimEnd = (s) => #- { return s.trimEnd() } -#

#elseif llvm

/**
 * Trim the leading and trailing whitespace
 * @since 0.18.7
 */
trim :: String -> String
export trim = extern "madlib__string__trim"

/**
 * Trim the leading whitespace
 * @since 0.18.7
 */
trimStart :: String -> String
export trimStart = extern "madlib__string__trimStart"

/**
 * Trim the trailing whitespace
 * @since 0.18.7
 */
trimEnd :: String -> String
export trimEnd = extern "madlib__string__trimEnd"

#endif


#iftarget js

/**
 * Return the length of a string
 * @since 0.18.7
 */
length :: String -> Integer
export length = (s) => #- { return [...s].length } -#

#elseif llvm

/**
 * Return the length of a string
 * @since 0.18.7
 */
length :: String -> Integer
export length = extern "madlib__string__length"

#endif

/**
 * Repeat a character a certain number of times
 * @since 0.18.7
 */
repeat :: Char -> Integer -> String
export repeat = (c, n) => pipe(
  List.repeat(c),
  fromList,
)(n)

/**
 * Pad the beginning of a string with a character up to a specific limit
 * @since 0.23.8
 * @example
 * padStart('-', 3, ">") // "-->"
 * padStart('-', 3, "cool") // "cool"
 */
padStart :: Char -> Integer -> String -> String
export padStart = (pre, count, str) => {
  len = length(str)
  return if (len > count) {
    str
  } else do {
    prefix = repeat(pre, count - len)
    return prefix ++ str
  }
}

/**
 * Pad the end of a string with a character up to a specific limit
 * @since 0.23.8
 * @example
 * padEnd('-', 3, "<") // "<--"
 * padEnd('-', 3, "cool") // "cool"
 */
padEnd :: Char -> Integer -> String -> String
export padEnd = (post, count, str) => {
  len = length(str)
  return if (len > count) {
    str
  } else do {
    suffix = repeat(post, count - len)
    return str ++ suffix
  }
}


#iftarget js

/**
 * Test whether a string matches a given regular expression
 * @since 0.18.7
 */
match :: String -> String -> Boolean
export match = (regex, input) => #- input.match(regex) !== null -#

/**
 * Replace part of a string given a regular expression
 * @since 0.18.7
 */
replace :: String -> String -> String -> String
export replace = (regex, replacing, input) => (
  #-
  input.replace(new RegExp(regex, "g"), replacing)
-#
)

#elseif llvm

/**
 * Test whether a string matches a given regular expression
 * @since 0.18.7
 */
match :: String -> String -> Boolean
export match = extern "madlib__string__match"

/**
 * Replace part of a string given a regular expression
 * @since 0.12.0
 */
replace :: String -> String -> String -> String
export replace = extern "madlib__string__replace"

#endif


#iftarget js

/**
 * Pushes a char at the beginning of a String
 * @since 0.12.0
 */
prependChar :: Char -> String -> String
export prependChar = (c, s) => #- { return c + s } -#

/**
 * Appends a char at the end of a String
 * @since 0.12.0
 */
appendChar :: Char -> String -> String
export appendChar = (c, s) => #- { return s + c } -#

#elseif llvm

/**
 * pushes a char at the beginning of a String
 * @since 0.12.0
 */
prependChar :: Char -> String -> String
export prependChar = extern "madlib__string__prependChar"

/**
 * appends a char at the end of a String
 * @since 0.12.0
 */
appendChar :: Char -> String -> String
export appendChar = extern "madlib__string__appendChar"

#endif


/**
 * Reverses the given string.
 *
 * @since 0.5.0
 * @example
 * reverse("abc") // "cba"
 */
reverse :: String -> String
export reverse = (s) => pipe(
  toList,
  List.reverse,
  fromList,
)(s)


/**
 * Returns true if the given character is in the given string.
 *
 * @since 0.18.6
 * @example
 * includes('b', "abüc") // true
 */
includes :: Char -> String -> Boolean
export includes = (c, s) => pipe(
  toList,
  List.includes(c),
)(s)


/**
 * Returns true if the second string starts with the second one.
 *
 * @since 0.18.6
 * @example
 * startsWith("ab", "abüc") // true
 * startsWith("bü", "abüc") // false
 */
startsWith :: String -> String -> Boolean
export startsWith = (subset, s) => pipe(
  toList,
  List.startsWith(toList(subset)),
)(s)


/**
 * Returns true if the first string is contained in the second one.
 *
 * @since 0.18.6
 * @example
 * contains("b", "abüc")  // true
 * contains("aü", "abüc") // false
 */
contains :: String -> String -> Boolean
export contains = (subset, s) => pipe(
  toList,
  List.contains(toList(subset)),
)(s)


/**
 * Returns true if the second string ends with the first one.
 *
 * @since 0.18.6
 * @example
 * endsWith("c", "abüc")  // true
 * endsWith("bü", "abüc") // false
 */
endsWith :: String -> String -> Boolean
export endsWith = (subset, s) => pipe(
  toList,
  List.endsWith(toList(subset)),
)(s)
