use crate::builtins::global::{js_to_number_value, throw_type_error_if_no_exception};
use crate::runtime::context::JSContext;
use crate::value::JSValue;

pub fn number_to_exponential(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
    let this = if args.is_empty() {
        return JSValue::new_string(ctx.intern("0"));
    } else {
        &args[0]
    };

    let fraction_digits = if args.len() > 1 && !args[1].is_undefined() {
        let d = args[1].to_number() as i32;
        if d < 0 || d > 100 {
            let err = JSValue::new_string(
                ctx.intern("RangeError: toExponential() argument must be between 0 and 100"),
            );
            ctx.pending_exception = Some(err);
            return JSValue::undefined();
        }
        d as usize
    } else {
        usize::MAX
    };

    let val = if this.is_int() {
        this.get_int() as f64
    } else if this.is_float() {
        this.get_float()
    } else if this.is_object() {
        let obj = this.as_object();
        if let Some(v) = obj.get(ctx.common_atoms.__value__) {
            if v.is_int() {
                v.get_int() as f64
            } else if v.is_float() {
                v.get_float()
            } else {
                return JSValue::new_string(ctx.intern("NaN"));
            }
        } else {
            let mut err = crate::object::object::JSObject::new();
            err.set(
                ctx.common_atoms.name,
                JSValue::new_string(ctx.intern("TypeError")),
            );
            err.set(
                ctx.common_atoms.message,
                JSValue::new_string(ctx.intern("this is not a Number")),
            );
            if let Some(proto) = ctx.get_type_error_prototype() {
                err.prototype = Some(proto);
            }
            let ptr = Box::into_raw(Box::new(err)) as usize;
            ctx.runtime_mut().gc_heap_mut().track(ptr);
            ctx.pending_exception = Some(JSValue::new_object(ptr));
            return JSValue::undefined();
        }
    } else {
        let mut err = crate::object::object::JSObject::new();
        err.set(
            ctx.common_atoms.name,
            JSValue::new_string(ctx.intern("TypeError")),
        );
        err.set(
            ctx.common_atoms.message,
            JSValue::new_string(ctx.intern("this is not a Number")),
        );
        if let Some(proto) = ctx.get_type_error_prototype() {
            err.prototype = Some(proto);
        }
        let ptr = Box::into_raw(Box::new(err)) as usize;
        ctx.runtime_mut().gc_heap_mut().track(ptr);
        ctx.pending_exception = Some(JSValue::new_object(ptr));
        return JSValue::undefined();
    };

    if val.is_nan() {
        return JSValue::new_string(ctx.intern("NaN"));
    }
    if val.is_infinite() {
        return JSValue::new_string(ctx.intern(if val.is_sign_positive() {
            "Infinity"
        } else {
            "-Infinity"
        }));
    }

    let result = if fraction_digits == usize::MAX {
        let s = format!("{:e}", val);

        fix_exponent_sign(&s)
    } else {
        let s = format!("{:.1$e}", val, fraction_digits);
        fix_exponent_sign(&s)
    };

    JSValue::new_string(ctx.intern(&result))
}

fn fix_exponent_sign(s: &str) -> String {
    let mut result = String::with_capacity(s.len() + 1);
    let mut chars = s.chars().peekable();
    let mut seen_e = false;
    while let Some(c) = chars.next() {
        if (c == 'e' || c == 'E') && !seen_e {
            seen_e = true;
            result.push(c);
            if let Some(&next) = chars.peek() {
                if next != '-' && next != '+' {
                    result.push('+');
                }
            }
        } else {
            result.push(c);
        }
    }
    result
}

pub fn number_to_string(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
    let this = if args.is_empty() {
        return JSValue::new_string(ctx.intern("0"));
    } else {
        &args[0]
    };

    let radix = if args.len() > 1 && !args[1].is_undefined() {
        let r = match js_to_number_value(ctx, &args[1]) {
            Ok(f) => f as i32,
            Err(()) => {
                return throw_type_error_if_no_exception(
                    ctx,
                    "toString() radix cannot be converted to a number",
                );
            }
        };
        if !(2..=36).contains(&r) {
            let mut err = crate::object::object::JSObject::new_typed(
                crate::object::object::ObjectType::Error,
            );
            err.set(
                ctx.common_atoms.name,
                JSValue::new_string(ctx.intern("RangeError")),
            );
            err.set(
                ctx.common_atoms.message,
                JSValue::new_string(ctx.intern("radix must be between 2 and 36")),
            );
            if let Some(proto) = ctx.get_range_error_prototype() {
                err.prototype = Some(proto);
            }
            let ptr = Box::into_raw(Box::new(err)) as usize;
            ctx.runtime_mut().gc_heap_mut().track(ptr);
            ctx.pending_exception = Some(JSValue::new_object(ptr));
            return JSValue::undefined();
        }
        r
    } else {
        10
    };

    let result = if this.is_int() {
        let n = this.get_int();
        if radix == 10 {
            n.to_string()
        } else if n < 0 {
            format!("-{}", format_radix((-n) as u64, radix as u32))
        } else {
            format_radix(n as u64, radix as u32)
        }
    } else if this.is_float() {
        let f = this.get_float();
        if f.is_nan() {
            "NaN".to_string()
        } else if f.is_infinite() {
            if f.is_sign_positive() {
                "Infinity".to_string()
            } else {
                "-Infinity".to_string()
            }
        } else if radix != 10 {
            let n = f as i64;
            if n < 0 {
                format!("-{}", format_radix((-n) as u64, radix as u32))
            } else {
                format_radix(n as u64, radix as u32)
            }
        } else if f == f.floor() && f.is_finite() && f.abs() < 1e15 {
            format!("{}", f as i64)
        } else {
            format!("{}", f)
        }
    } else if this.is_object() {
        let obj = this.as_object();
        let number_proto = ctx.get_number_prototype();
        let mut is_number_wrapper = false;
        if let Some(np) = number_proto {
            let this_ptr = this.get_ptr();
            if this_ptr as usize == np as usize {
                is_number_wrapper = true;
            }
            if !is_number_wrapper {
                let mut current = this.as_object().prototype;
                let mut depth = 0u32;
                while let Some(p) = current {
                    if p.is_null() || depth > 10 {
                        break;
                    }
                    if p as usize == np as usize {
                        is_number_wrapper = true;
                        break;
                    }
                    unsafe {
                        current = (*p).prototype;
                    }
                    depth += 1;
                }
            }
        }
        if !is_number_wrapper {
            return throw_type_error_if_no_exception(
                ctx,
                "Number.prototype.toString requires 'this' to be a Number",
            );
        }
        if let Some(v) = obj.get(ctx.common_atoms.__value__) {
            if v.is_int() {
                let n = v.get_int();
                if radix == 10 {
                    n.to_string()
                } else if n < 0 {
                    format!("-{}", format_radix((-n) as u64, radix as u32))
                } else {
                    format_radix(n as u64, radix as u32)
                }
            } else if v.is_float() {
                let f = v.get_float();
                if f.is_nan() {
                    "NaN".to_string()
                } else if f.is_infinite() {
                    if f.is_sign_positive() {
                        "Infinity".to_string()
                    } else {
                        "-Infinity".to_string()
                    }
                } else if radix != 10 {
                    let n = f as i64;
                    if n < 0 {
                        format!("-{}", format_radix((-n) as u64, radix as u32))
                    } else {
                        format_radix(n as u64, radix as u32)
                    }
                } else if f == f.floor() && f.is_finite() && f.abs() < 1e15 {
                    format!("{}", f as i64)
                } else {
                    format!("{}", f)
                }
            } else {
                return throw_type_error_if_no_exception(
                    ctx,
                    "Number.prototype.toString requires 'this' to be a Number",
                );
            }
        } else {
            return throw_type_error_if_no_exception(
                ctx,
                "Number.prototype.toString requires 'this' to be a Number",
            );
        }
    } else {
        return throw_type_error_if_no_exception(
            ctx,
            "Number.prototype.toString requires 'this' to be a Number",
        );
    };
    JSValue::new_string(ctx.intern(&result))
}

pub(crate) fn format_radix(mut n: u64, radix: u32) -> String {
    if n == 0 {
        return "0".to_string();
    }
    const DIGITS: &[u8] = b"0123456789abcdefghijklmnopqrstuvwxyz";
    let mut result = Vec::new();
    while n > 0 {
        result.push(DIGITS[(n % radix as u64) as usize]);
        n /= radix as u64;
    }
    result.reverse();
    String::from_utf8(result).unwrap()
}
