
use mem
use utils
use gc

+ class String imut $noNewProperties {
    // Properties
    ~ bytes: uint

    + get length : uint {
        return this.bytes
    }

    + get data : ptr[u8] {
        return this.@cast(ptr) + size_of(<SELF>)
    }
    + get data_cstring : cstring {
        return this.data.@cast(cstring)
    }

    /////////////////////
    // Start / Ends
    /////////////////////

    + fn starts_with(part: SELF) bool {
        if part.bytes > this.bytes : return false
        return mem:equal(this.data, part.data, part.bytes)
    }
    + fn ends_with(part: SELF) bool {
        if part.bytes > this.bytes : return false
        return mem:equal(@offset(this.data, u8,  this.bytes - part.bytes), part.data, part.bytes)
    }

    + fn is_empty() bool { return this.bytes == 0 }

    /////////////////////
    // Convert
    /////////////////////

    + fn to_uint() uint !invalid {
        return this.data.read_uint_value(this.bytes) ! throw invalid
    }
    + fn to_int() int !invalid {
        if this.to(String).get(0) == '-' : return (((this.data + 1).@cast(ptr)).read_uint_value(this.bytes - 1) ! throw invalid).@cast(int) * -1
        return (this.data.read_uint_value(this.bytes) ! throw invalid).@cast(int)
    }

    + fn hex_to_uint() uint !invalid {
        return this.data.read_hex_value(this.bytes) ! throw invalid
    }
    + fn hex_to_int() int !invalid {
        if this.to(String).get(0) == '-' : return (((this.data + 1).@cast(ptr)).read_hex_value(this.bytes - 1) ! throw invalid).@cast(int) * -1
        return (this.data.read_hex_value(this.bytes) ! throw invalid).@cast(int)
    }

    + fn octal_to_uint() uint !invalid {
        return this.data.read_octal_value(this.bytes) ! throw invalid
    }
    + fn octal_to_int() int !invalid {
        if this.to(String).get(0) == '-' : return (((this.data + 1).@cast(ptr)).read_octal_value(this.bytes - 1) ! throw invalid).@cast(int) * -1
        return (this.data.read_octal_value(this.bytes) ! throw invalid).@cast(int)
    }

    + fn to_float() f64 !invalid {

        let result : f64 = 0
        let sign : f64 = 1
        let i : uint = 0
        let len = this.bytes
        let has_dot = false
        let decimals = 0
        let ascii = this.to(String)

        if i == len : throw invalid
        if ascii.get(0) == '-' {
            sign = -1
            i++
            if i == len : throw invalid
        }

        while i < len {
            let ch = ascii.get(i++)
            if ch.is_number() {
                result = result * 10 + (ch - '0');
                if has_dot : decimals++
            } else if ch == '.' {
                if has_dot : throw invalid
                has_dot = true
            } else {
                throw invalid
            }
        }

        while decimals-- > 0 {
            result = result / 10
        }

        return result * sign
    }

    + fn lower() SELF {
        let len = this.bytes
        let data = this.data
        let new_str = SELF.make_empty(len)
        let new_data = new_str.data
        while len-- > 0 {
            let ch = @ptrv(data, u8, len)
            if ch > 64 && ch < 91 : ch += 32
            @ptrv(new_data, u8, len) = ch
        }
        return new_str
    }

    + fn upper() SELF {
        let len = this.bytes
        let data = this.data
        let new_str = SELF.make_empty(len)
        let new_data = new_str.data
        while len-- > 0 {
            let ch = @ptrv(data, u8, len)
            if ch > 96 && ch < 123 : ch -= 32
            @ptrv(new_data, u8, len) = ch
        }
        return new_str
    }

	/////////////////////
	// Syntax
	/////////////////////

    + fn is_alpha(allow_extra_bytes: SELF ("")) bool {
		let i : uint = 0
		let len = this.bytes
		let data = this.data
		while i < len {
			let ch = @ptrv(data, u8, i++)
			if (ch < 'A' || ch > 'Z') && (ch < 'a' || ch > 'z') {
				if !allow_extra_bytes.contains_byte(ch) : return false
			}
		}
		return true
	}
    + fn is_alpha_numeric(allow_extra_bytes: SELF ("")) bool {
		let i : uint = 0
		let len = this.bytes
		let data = this.data
		while i < len {
			let ch = @ptrv(data, u8, i++)
			if (ch < 'A' || ch > 'Z') && (ch < 'a' || ch > 'z') && (ch < '0' || ch > '9') {
				if !allow_extra_bytes.contains_byte(ch) : return false
			}
		}
		return true
	}

    + fn is_integer() bool {
		let i : uint = 0
		let len = this.bytes
        if len == 0 : return false
		let data = this.data
		while i < len {
			let ch = @ptrv(data, u8, i++)
			if !ch.is_number() : return false
		}
		return true
	}

    + fn is_number() bool {
		let i : uint = 0
		let len = this.bytes
        if len == 0 : return false
        let dot_count = 0
		let data = this.data
		while i < len {
			let ch = @ptrv(data, u8, i++)
            if ch == '.' {
                if dot_count++ > 0 : return false
                continue
            }
			if !ch.is_number() : return false
		}
		return true
	}

    /////////////////////
    // Internals
    /////////////////////

    fn add(add: SELF) SELF $add {
        let this_len = this.bytes
        let add_len = add.bytes
        let len = this_len + add_len
        if len == 0 : return "".to(SELF)
        let s = SELF.make_empty(len)
        let data = s.data
        mem:copy(this.data, data, this_len)
        mem:copy(add.data, @offset(data, u8, this_len), add_len + 1) // + 1 for 0 byte
        return s
    }

    /////////////////////
    // Compare
    /////////////////////
    // static fn equal(this: ?SELF, that: ?SELF) bool $eq {
    //     if isset(this) != isset(that) : return false
    //     if !isset(this) || !isset(that) : return false
    //     if this.bytes != that.bytes : return false 
    //     return mem:equal(this.data, that.data, this.bytes)
    // }

    fn equal(cmp: SELF) bool $eq {
        if this.bytes != cmp.bytes : return false 
        return mem:equal(this.data, cmp.data, this.bytes)
    }

    fn lt(cmp: SELF) bool $lt {
        let len1 = this.bytes
        let len2 = cmp.bytes
        let len = len1 <= len2 ? len1 : len2
        let data1 = this.data
        let data2 = cmp.data
        let i : uint = 0
        while i < len {
            let ch1 = @ptrv(data1, u8, i)
            let ch2 = @ptrv(data2, u8, i++)
            if ch1 != ch2 : return ch1 < ch2
        }
        return len1 <= len2
    }
    fn gt(cmp: SELF) bool $gt {
        let len1 = this.bytes
        let len2 = cmp.bytes
        let len = len1 <= len2 ? len1 : len2
        let data1 = this.data
        let data2 = cmp.data
        let i : uint = 0
        while i < len {
            let ch1 = @ptrv(data1, u8, i)
            let ch2 = @ptrv(data2, u8, i++)
            if ch1 != ch2 : return ch1 > ch2
        }
        return len1 > len2
    }
    fn lte(cmp: SELF) bool $lte {
        return this == cmp || this < cmp
    }
    fn gte(cmp: SELF) bool $gte {
        return this == cmp || this > cmp
    }

    static fn default_array_filter(i: ?String) bool {
        if !isset(i) : return true
        return i.bytes == 0
    }

    fn map_hash() uint {
        let len = this.bytes
        let data = this.data
        let hash : uint = 1337
        if len > 0 {
            hash += @ptrv(data, u8, 0)
            hash += @ptrv(data, u8, len - 1)
        }
        return hash
    }

    /////////////////////
    // Create
    /////////////////////

    fn clone() SELF {
        return SELF.make_from_ptr(this.data, this.bytes)
    }

    + static fn make_empty(length: uint) SELF {
        if length == 0 : return "".to(SELF)
        let d = gc:alloc(size_of(<SELF>) + length + 1).@cast(SELF)
        @ptrv(@ref(d.bytes), uint) = length
        @ptrv(d.@cast(ptr), u8, length + size_of(<SELF>)) = 0
        return d
    }

    + static fn make_from_ptr(data: ptr, length: uint) SELF {
        if length == 0 : return "".to(SELF)
        let s = SELF.make_empty(length)
        let str_data = s.data
        mem:copy(data, str_data, length)
        @ptrv(str_data, u8, length) = 0 // TODO: remove line
        return s
    }

    /////////////////////
    // Index of
    /////////////////////

    + fn index_of(part: String, start_index: uint (0)) uint !not_found {
        let part_bytes = part.bytes
        if part_bytes > this.bytes : throw not_found
        let index = start_index
        let len = this.bytes - part_bytes + 1
        let data = this.data
        let part_data = part.data
        while index < len {
            if mem:equal(@offset(data, u8, index), part_data, part_bytes) : return index
            index++
        }
        throw not_found
    }

    + fn index_of_byte(byte: u8, start_index: uint (0)) uint !not_found {
        let index = start_index
        let len = this.bytes
        let data = this.data
        while index < len {
            let ch = @ptrv(data, u8, index)
            if ch == byte : return index
            index++
        }
        throw not_found
    }

    + fn contains(part: String) bool {
        this.index_of(part) ! return false
        return true
    }
    + fn contains_byte(byte: u8) bool {
        this.index_of(byte) ! return false
        return true
    }

    /////////////////////
    // Partials
    /////////////////////
    
    + fn get(index: uint) u8 $offset {
        if index >= this.bytes : return 0
        return this.data[index]
    }

    + fn range(start: uint, end: uint, inclusive: bool (true)) SELF {
        end = end + (inclusive).to(uint)
        if start >= end : return ""
        return this.part(start, end - start)
    }

    + fn part(start_index: uint, length: uint) String {
        let this_bytes = this.bytes
        if start_index > this_bytes : return ""
        if (start_index + length) > this_bytes {
            length = this_bytes - start_index
        }
        if length == 0 : return ""
        let from = @offset(this.data, u8,  start_index)
        let res = String.make_empty(length)
        mem:copy(from, res.data, length)
        return res
    }

    + fn split(on: String) Array[String] {
        let result = Array[String].new()
        let start : uint = 0
        let on_len = on.bytes
        if on_len == 0 {
            let this_len = this.length
            let i : uint = 0
            while i < this_len {
                result.append(this.part(i++, 1))
            }
        } else {
            while true {
                let index = this.index_of(on, start) ! break
                let part = this.part(start, index - start)
                result.append(part)
                start = index + on_len
            }
            let part = this.part(start, this.bytes - start)
            result.append(part)
        }
        return result
    }

    fn split_on_first_occurance_of_byte(byte: u8) (String, String) !not_found {
        let pos = this.index_of_byte(byte) ! throw not_found
        let p2 = this.part(pos + 1, this.bytes - pos - 1)
        let p1 = this.part(0, pos)
        return p1, p2
    }


    /////////////////////
    // Trim
    /////////////////////

    + fn trim(part: SELF, limit: uint (0)) SELF {
        let res = this.ltrim(part, limit)
        return res.rtrim(part, limit)
    }

    + fn rtrim(part: SELF, limit: uint (0)) SELF {

        let part_bytes = part.bytes
        let part_data = part.data

        let data = this.data
        let len = this.bytes
        let data_end = data + len
        let offset : uint = 0
        let count : uint = 0
        while (offset + part_bytes <= len) && mem:equal(data_end - offset - part_bytes, part_data, part_bytes) {
            offset += part_bytes
            count++
            if count == limit : break
        }
        if offset == 0 : return this

        return SELF.make_from_ptr(data, len - offset)
    }

    + fn ltrim(part: SELF, limit: uint (0)) SELF {
        let part_bytes = part.bytes
        let part_data = part.data

        let data = this.data
        let len = this.bytes
        let offset : uint = 0
        let count : uint = 0
        while (offset + part_bytes <= len) && mem:equal(data + offset, part_data, part_bytes) {
            offset += part_bytes
            count++
            if count == limit : break
        }
        if offset == 0 : return this

        return SELF.make_from_ptr(data + offset, len - offset)
    }

    /////////////////////
    // Modify
    /////////////////////

    + fn replace(part: String, with: String) String {
        let len = this.bytes
        if len == 0 : return ""

        let part_bytes = part.bytes
        let part_data = part.data

        let buf = utils:ByteBuffer.new(len)
        let data = this.data
        let offset : uint = 0
        while offset < len {
            if (offset + part_bytes) <= len && mem:equal(data + offset, part_data, part_bytes) {
                buf.append_str(with)
                offset += part_bytes
                continue
            }
            buf.append_byte(data[offset++])
        }

        return buf
    }

    + fn pad_left(char: u8, length: uint) String {
        if this.length >= length : return this
        let chars = length - this.length
        let str = SELF.make_empty(length)
        let data = str.data
        mem:copy(this.data, data + chars, this.length)
        while chars-- > 0 {
            @ptrv(data, u8, chars) = char
        }
        return str
    }

    + fn pad_right(char: u8, length: uint) String {
        let len = this.length
        if len >= length : return this
        let chars = length - len
        let str = SELF.make_empty(length)
        let data = str.data
        mem:copy(this.data, data, len)
        while chars > 0 {
            @ptrv(data, u8, len + chars--) = char
        }
        return str
    }

    /////////////////////
    // Misc
    /////////////////////

    + fn escape() String {
        let bytes = this.bytes
        if bytes == 0 : return ""
        let data = this.data
        let buf = utils:ByteBuffer.new(bytes)
        //
        let i : uint = 0
        while i < bytes {
            let ch = @ptrv(data, u8, i++)
            if (ch == '\n') : buf.append_str("\\n")
            else if (ch == '\r') : buf.append_str("\\r")
            else if (ch == '\t') : buf.append_str("\\t")
            else if (ch == '\f') : buf.append_str("\\f")
            else if (ch == '\b') : buf.append_str("\\b")
            else if (ch == '\v') : buf.append_str("\\v")
            else if (ch == '\f') : buf.append_str("\\f")
            else if (ch == '\a') : buf.append_str("\\a")
            else if (ch == '\e') : buf.append_str("\\e")
            else if (ch == '\\') : buf.append_str("\\\\")
            else if (ch == '"') : buf.append_str("\\\"")
            else : buf.append_byte(ch)
        }
        return buf.to_string()
    }

    // Convert escaped characters
    + fn unescape() String {
        let bytes = this.bytes
        if bytes == 0 : return ""
        let data = this.data
        let buf = utils:ByteBuffer.new(bytes)
        //
        let i : uint = 0
        while i < bytes {
            let ch = @ptrv(data, u8, i++)
            if (ch == '\\') {
                let ch = @ptrv(data, u8, i++)
                ch = ch.unescape()
                buf.append_byte(ch)
                continue
            }
            buf.append_byte(ch)
        }
        return buf.to_string()
    }

    fn each_get(index: uint) u8 !end {
        if index >= this.bytes : throw end
        return this.data[index]
    }

    /////////////////////
    // UTF8
    /////////////////////

    group utf8 {

        + get length : uint {
            let step = @stack(<CharStep>)
            step.set(this)
            let count : uint = 0
            while true {
                step.next() ! return count
                count++
            }
            return 0
        }

        /////////////////////
        // Index of
        /////////////////////

        + fn index_of(part: String, start_index: uint (0)) uint !not_found {
            if part.bytes > this.bytes : throw not_found
            let step = @stack(<CharStep>)
            step.set(this)
            let count : uint = 0
            let offset : uint = 0
            let adr = this.data
            let part_adr = part.data
            let part_bytes = part.bytes
            let last_offset = this.bytes - part_bytes
            while count < start_index {
                offset += step.next() ! throw not_found
                if offset > last_offset : throw not_found
                count++
            }
            while true {
                if mem:equal(@offset(adr, u8, offset), part_adr, part_bytes) : return count
                offset += step.next() ! throw not_found
                if offset > last_offset : throw not_found
                count++
            }
            return 0
        }

        + fn contains(part: String) bool {
            this.index_of(part) ! return false
            return true
        }

        /////////////////////
        // Partials
        /////////////////////

        + fn get(index: uint) String {
            return this.part(index, 1)
        }

        + fn range(start: uint, end: uint, inclusive: bool (true)) SELF {
            end = end + (inclusive).to(uint)
            if start >= end : return ""
            return this.utf8.part(start, end - start)
        }

        + fn part(start_index: uint, length: uint) String {

            if length == 0 : return ""
            let this_bytes = this.bytes
            if start_index > this_bytes : return ""

            let step = @stack(<CharStep>)
            step.set(this)

            let count : uint = 0
            let offset : uint = 0
            let data = this.data
            while count < start_index {
                offset += step.next() ! return ""
                count++
            }
            let start = offset
            count = 0
            while count++ < length {
                offset += step.next() ! break
                if offset >= this_bytes : break
            }
            if offset > this_bytes : offset = this_bytes
            return String.make_from_ptr(@offset(data, u8, start), offset - start)
        }

        + fn split(on: String) Array[String] {
            let result = Array[String].new()
            let on_bytes = on.bytes
            let this_bytes = this.bytes
            if on_bytes > this_bytes || on_bytes == 0 {
                result.append(this)
                return result
            }

            let step = @stack(<CharStep>)
            step.set(this)

            let offset : uint = 0
            let start : uint = 0

            let data = this.data
            let on_data = on.data
            let last_offset = this_bytes - on_bytes
            while offset <= last_offset {
                if mem:equal(@offset(data, u8, offset), on_data, on_bytes) {
                    result.append(String.make_from_ptr(@offset(data, u8, start), offset - start))
                    offset += on_bytes
                    start = offset
                    step.skip_bytes(on_bytes)
                    continue
                }
                offset += step.next() ! break
            }
            if offset > this_bytes : offset = this_bytes
            result.append(String.make_from_ptr(@offset(data, u8, start), offset - start))
            return result
        }
    }

    /////////////////////
    // ANSI
    /////////////////////

    group ansi {
        fn esc() String {
            return 27.to(u8).to_ascii_string()
        }
        fn reset() String {
            return 27.to(u8).to_ascii_string() + "[0m"
        }

        + fn black(bold: bool (false)) String {
            return this.ansi.esc() + "[" + (bold ? 1 : 0) + ";30m" + this + this.ansi.reset()
        }
        + fn red(bold: bool (false)) String {
            return this.ansi.esc() + "[" + (bold ? 1 : 0) + ";31m" + this + this.ansi.reset()
        }
        + fn green(bold: bool (false)) String {
            return this.ansi.esc() + "[" + (bold ? 1 : 0) + ";32m" + this + this.ansi.reset()
        }
        + fn yellow(bold: bool (false)) String {
            return this.ansi.esc() + "[" + (bold ? 1 : 0) + ";33m" + this + this.ansi.reset()
        }
        + fn blue(bold: bool (false)) String {
            return this.ansi.esc() + "[" + (bold ? 1 : 0) + ";34m" + this + this.ansi.reset()
        }
        + fn purple(bold: bool (false)) String {
            return this.ansi.esc() + "[" + (bold ? 1 : 0) + ";35m" + this + this.ansi.reset()
        }
        + fn cyan(bold: bool (false)) String {
            return this.ansi.esc() + "[" + (bold ? 1 : 0) + ";36m" + this + this.ansi.reset()
        }
        + fn white(bold: bool (false)) String {
            return this.ansi.esc() + "[" + (bold ? 1 : 0) + ";37m" + this + this.ansi.reset()
        }
    }
}
