/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved.
 * This source file is part of the Cangjie project, licensed under Apache-2.0
 * with Runtime Library Exception.
 *
 * See https://cangjie-lang.cn/pages/LICENSE for license information.
 */

// The Cangjie API is in Beta. For details on its capabilities and limitations, please refer to the README file.

package std.unittest.prop_test

func coerceInRange(value: Byte, min: Byte, max: Byte) {
    if (min <= value && value <= max) { return value }
    return value % (max - min + 1) + min
}

func nextIn<T>(random: RandomSource, arr: Array<T>): T {
    let ix = random.nextInt64(arr.size)
    return arr[ix]
}
/**
 * A new random byte in range [low, high], both inclusive
 */
// @OverflowWrapping is not needed here because it cannot overflow,
// but on CJVM that throws overflow exception
@OverflowWrapping
func nextInRange(random: RandomSource, low: Byte, high: Byte): Byte {
    let tri = random.nextUInt8()
    if (low <= tri && tri <= high) { return tri }
    return tri % (high - low + 1) + low
}
/**
 * Here we encode each state of UTF-8 state machine (see below)
 * as a jump table (data field) that associates each byte with a new state
 * and a set or byte ranges (ranges field) which encodes which ranges of bytes
 * are valid in that state
 */
struct UTFState {
    init() {
        this.data = []
        this.ranges = []
    }
    init(rangeTransfer: Array<(Byte, Byte, UTFState)>) {
        this.data = Array(256) { ix: Int64 =>
            let byte = UInt8(ix)
            for ((low, high, next) in rangeTransfer) {
                if (low <= byte && byte <= high) {
                    return next
                }
            }
            return UTFState()
        }
        this.ranges = Array(rangeTransfer.size) { ix =>
            let (low, high, _) = rangeTransfer[ix]
            (low, high)
        }
    }
    init(low: Byte, high: Byte, next: UTFState) {
        this.data = Array(256) { ix: Int64 =>
            let byte = UInt8(ix)
            if (low <= byte && byte <= high) { next } else { UTFState() }
        }
        this.ranges = [(low, high)]
    }

    let data: Array<UTFState>
    let ranges: Array<(Byte, Byte)>

    func isInvalid() { data.isEmpty() }

    operator func ()(byte: Byte): UTFState {
        data[Int64(byte)]
    }

    func nextByte(random: RandomSource) {
        let (low, high) = nextIn(random, ranges)
        return nextInRange(random, low, high)
    }
    /**
     * The canonical UTF8 state machine
     * [START]+--{00..7F}--------------------------------------------------------+
     *        |--{C2..DF}--------------------------------------+                 |
     *        |--{E0}-----> [C] --{A0..BF}---------------------|                 |
     *        |--{ED}-----> [D] --{A0..9F}---------------------|                 |
     *        |--{E1..EC, EE..EF}-------------+                |                 |
     *        |--{F1..F3}-> [E] --{80..BF}----|                |                 |
     *        |--{F0}-----> [F] --{90..BF}----|                |                 |
     *        |                               v                v                 v
     *        +--{F4}-----> [G] --{80..8F}-> [B] --{80..BF}-> [A] --{80..BF}-> [END]
     * (all other transitions are invalid)
     **/
    static let END_STATE = UTFState()
    static let A_STATE = UTFState(0x80, 0xBF, END_STATE)
    static let B_STATE = UTFState(0x80, 0xBF, A_STATE)
    static let C_STATE = UTFState(0xA0, 0xBF, A_STATE)
    static let D_STATE = UTFState(0x80, 0x9F, A_STATE)
    static let E_STATE = UTFState(0x80, 0xBF, B_STATE)
    static let F_STATE = UTFState(0x90, 0xBF, B_STATE)
    static let G_STATE = UTFState(0x80, 0x8F, B_STATE)
    static let START_STATE = UTFState(
        (0x00, 0x7F, END_STATE),
        (0xC2, 0xDF, A_STATE),
        (0xE1, 0xEC, B_STATE),
        (0xEE, 0xEF, B_STATE),
        (0xE0, 0xE0, C_STATE),
        (0xED, 0xED, D_STATE),
        (0xF1, 0xF3, E_STATE),
        (0xF0, 0xF0, F_STATE),
        (0xF4, 0xF4, G_STATE)
    )
}

let ALNUM_CHARS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".toArray()

func randomAlnumRune(random: RandomSource): Rune {
    let candidate = random.nextUInt8()
    match {
        case b'0' <= candidate && candidate <= b'9'
            || b'a' <= candidate && candidate <= b'z'
            || b'A' <= candidate && candidate <= b'Z' => Rune(candidate)
        case _ => Rune(ALNUM_CHARS[Int64(candidate) % ALNUM_CHARS.size])
    }
}

private const ASCII_PRINTABLE_MIN: Byte = 0x20
private const ASCII_PRINTABLE_MAX: Byte = 0x7E

func randomAsciiRune(random: RandomSource): Rune {
    Rune(nextInRange(random, ASCII_PRINTABLE_MIN, ASCII_PRINTABLE_MAX))
}

func randomUtf8Rune(random: RandomSource): Rune {
    var state = UTFState.START_STATE
    let result = Array<Byte>(6, repeat: 0)
    var i = 0
    while (!state.isInvalid()) {
        let byte = state.nextByte(random)
        state = state(byte)
        result[i] = byte
        i++
    }

    return Rune.fromUtf8(result[..i], 0)[0]
}

func randomBiasedRune(random: RandomSource): Rune {
    match (random.nextInt64(3)) {
        case 0 => randomAlnumRune(random)
        case 1 => randomAsciiRune(random)
        case 2 => randomUtf8Rune(random)
        case _ => throw IllegalArgumentException()
    }
}

func randomSuggestedRune(random: RandomSource): Rune {
    let byte = random.suggestUInt8()
    if (ASCII_PRINTABLE_MIN <= byte && byte <= ASCII_PRINTABLE_MAX) {
        return Rune(byte)
    }
    // utf-8 is too complex to do anything smart
    return randomUtf8Rune(random)
}

func shrinkToAscii(charValue: Rune): Rune {
    if (charValue.isAscii()) {
        return charValue
    }
    let min = UInt32(ASCII_PRINTABLE_MIN)
    let max = UInt32(ASCII_PRINTABLE_MAX)
    let cp = UInt32(charValue) % (max - min + 1) + min
    Rune(cp)
}

func shrinkToAlnum(charValue: Rune): Rune {
    if (charValue.isAsciiLetter() || charValue.isAsciiNumber()) {
        return charValue
    }
    let alnumByte = ALNUM_CHARS[charValue.hashCode() % ALNUM_CHARS.size]
    Rune(alnumByte)
}