/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

//! Visitor structures and helpers for the AST.

use crate::GCLock;
use crate::Node;
use crate::NodeField;

/// Indicates the path to the current node.
#[derive(Debug, Copy, Clone)]
pub struct Path<'a> {
    /// Parent node.
    pub parent: &'a Node<'a>,

    /// Field name in the path node.
    pub field: NodeField,
}

impl<'a> Path<'a> {
    pub fn new(parent: &'a Node<'a>, field: NodeField) -> Path<'a> {
        Path { parent, field }
    }
}

/// Trait implemented by those who call the visit functionality.
pub trait Visitor<'gc> {
    /// Visit the Node `node` with the given `path`.
    fn call(&mut self, ctx: &'gc GCLock, node: &'gc Node<'gc>, path: Option<Path<'gc>>);
}

/// Indicates what mutation occurred to an element of the AST during [`VisitorMut`] use.
#[derive(Debug)]
pub enum TransformResult<T> {
    /// No change to the element.
    Unchanged,

    /// Element must be removed, if possible.
    /// If the element cannot be removed, it will be replaced with EmptyStatement.
    /// **Only statements may be removed from non-optional fields,
    /// as removing expressions would result in an invalid AST.**
    Removed,

    /// Element should be swapped out for the wrapped element.
    Changed(T),

    /// Element should be swapped out for multiple wrapped elements.
    Expanded(Vec<T>),
}

/// Trait implemented by those who call the visit functionality.
pub trait VisitorMut<'gc> {
    /// Visit the Node `node` with the given `path`.
    fn call(
        &mut self,
        ctx: &'gc GCLock,
        node: &'gc Node<'gc>,
        path: Option<Path<'gc>>,
    ) -> TransformResult<&'gc Node<'gc>>;
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

fn main() {
    if cfg!(target_os = "macos") {
        println!("cargo:rustc-link-arg=-Wl,-stack_size,0x1000000");
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

use std::collections::HashMap;
use std::fs::File;
use std::io::Write;
use std::ops::DerefMut;
use std::path::Path;
use std::path::PathBuf;
use std::process::exit;
use std::rc::Rc;
use std::str::FromStr;

use anyhow::ensure;
use anyhow::Context;
use anyhow::Error;
use command_line::CommandLine;
use command_line::Hidden;
use command_line::Opt;
use command_line::OptDesc;
use juno::ast;
use juno::ast::node_cast;
use juno::ast::validate_tree;
use juno::ast::NodeRc;
use juno::ast::SourceRange;
use juno::gen_js;
use juno::hparser;
use juno::hparser::MagicCommentKind;
use juno::hparser::ParsedJS;
use juno::hparser::ParserDialect;
use juno::resolve_dependency;
use juno::sema;
use juno::sema::SemContext;
use juno::sourcemap::merge_sourcemaps;
use juno_pass::PassManager;
use juno_support::fetchurl;
use juno_support::source_manager::SourceId;
use juno_support::HeapSize;
use juno_support::NullTerminatedBuf;
use juno_support::Timer;
use sourcemap::SourceMap;
use url::Url;

#[derive(Debug, Copy, Clone, PartialEq)]
enum Gen {
    /// Dump the Semantic resolution information.
    Sema,
    /// Dump the AST as JSON.
    Ast,
    /// Generate JavaScript source.
    Js,
    /// Generate JavaScript source with annotations about variable resolution.
    ResolvedJs,
}

#[derive(Debug, Copy, Clone, PartialEq)]
enum InputSourceMap {
    Ignore,
    Auto,
}

struct Options {
    /// Enable pretty printing.
    pretty: Opt<bool>,

    /// Select what to emit.
    gen: Opt<Gen>,

    /// Perform AST validation.
    validate_ast: Opt<bool>,

    /// Perform semantic analysis.
    sema: Opt<bool>,

    /// Input file to parse.
    input_path: Opt<PathBuf>,

    /// Path to output to.
    /// Defaults to `-`, which is `stdout`.
    // #[structopt(long = "out", short = "o", default_value = "-", parse(from_os_str))]
    output_path: Opt<PathBuf>,

    /// Whether to output a source map.
    /// The source map will be merged with an input source map if provided.
    /// Can only be used when generating JS.
    sourcemap: Opt<bool>,

    /// Base URL to prepend to relative URLs.
    base_url: Opt<Option<Url>>,

    /// How to handle the source map directives.
    input_source_map: Opt<InputSourceMap>,

    /// Whether to run optimization passes.
    optimize: Opt<bool>,

    /// Whether to run strip flow types.
    strip_flow: Opt<bool>,

    /// Whether to force a space after the `async` keyword in arrow functions.
    force_async_arrow_space: Opt<bool>,

    /// Whether to emit the doc block when generating JS.
    /// The doc block contains every comment prior to the first non-directive token in the file.
    emit_doc_block: Opt<bool>,

    /// Whether to use double quotes on string literals.
    double_quote_strings: Opt<bool>,

    /// Whether to run the parsed AST.
    run: Opt<bool>,

    /// Control the recognized JavaScript dialect.
    dialect: Opt<ParserDialect>,

    /// Enable JSX parsing.
    jsx: Opt<bool>,

    /// Enable strict mode.
    strict_mode: Opt<bool>,

    /// Warn about undefined variables in strict mode functions.
    warn_undefined: Opt<bool>,

    /// Measure and print times.
    xtime: Opt<bool>,

    /// Measure and print memory.
    xmem: Opt<bool>,
}

impl Options {
    pub fn new(cl: &mut CommandLine) -> Options {
        let input_cat = cl.add_category("Input Options", None);
        let output_cat = cl.add_category("Output Options", None);

        Options {
            pretty: Opt::new_bool(
                cl,
                OptDesc {
                    long: Some("pretty"),
                    desc: Some("Enable pretty printing (default: on)."),
                    init: Some(true),
                    category: output_cat,
                    ..Default::default()
                },
            ),
            gen: Opt::new_enum(
                cl,
                OptDesc {
                    desc: Some("Choose generated output:"),
                    values: Some(&[
                        ("gen-sema", Gen::Sema, "Dump the Sema data."),
                        ("gen-ast", Gen::Ast, "Dump the AST as JSON."),
                        ("gen-js", Gen::Js, "Generate JavaScript source."),
                        (
                            "gen-resolved-js",
                            Gen::ResolvedJs,
                            "Generate resolution information.",
                        ),
                    ]),
                    category: output_cat,
                    ..Default::default()
                },
            ),
            validate_ast: Opt::new_bool(
                cl,
                OptDesc {
                    long: Some("validate-ast"),
                    desc: Some("Perform AST validation (default: on)."),
                    init: Some(true),
                    ..Default::default()
                },
            ),
            sema: Opt::new_bool(
                cl,
                OptDesc {
                    long: Some("sema"),
                    desc: Some("Perform semantic analysis (default: on)."),
                    init: Some(true),
                    ..Default::default()
                },
            ),
            input_path: Opt::<PathBuf>::new(
                cl,
                OptDesc {
                    desc: Some("'input-path'"),
                    min_count: 1,
                    list: true,
                    ..Default::default()
                },
            ),
            output_path: Opt::<PathBuf>::new(
                cl,
                OptDesc {
                    long: Some("out"),
                    short: Some("o"),
                    desc: Some("Path to output to. Defaults to `-`, which is `stdout`"),
                    init: Some(PathBuf::from_str("-").unwrap()),
                    value_desc: Some("output path"),
                    category: output_cat,
                    ..Default::default()
                },
            ),
            sourcemap: Opt::new_bool(
                cl,
                OptDesc {
                    long: Some("sourcemap"),
                    desc: Some("Whether to generate a source map."),
                    category: output_cat,
                    ..Default::default()
                },
            ),
            base_url: Opt::<Option<Url>>::new_optional(
                cl,
                OptDesc {
                    long: Some("base-url"),
                    desc: Some("Base URL to prepend to relative URLs."),
                    value_desc: Some("URL"),
                    category: input_cat,
                    ..Default::default()
                },
            ),
            input_source_map: Opt::new_enum(
                cl,
                OptDesc {
                    long: Some("input-source-map"),
                    desc: Some("How to handle the source map directives (default: auto)."),
                    values: Some(&[
                        (
                            "ignore",
                            InputSourceMap::Ignore,
                            "Ignore //# sourceMappingURL directives",
                        ),
                        (
                            "auto",
                            InputSourceMap::Auto,
                            "Recognize //# sourceMappingURL directives",
                        ),
                    ]),
                    category: input_cat,
                    ..Default::default()
                },
            ),
            optimize: Opt::new_flag(
                cl,
                OptDesc {
                    short: Some("O"),
                    desc: Some("Run optimization passes."),
                    ..Default::default()
                },
            ),
            strip_flow: Opt::new_bool(
                cl,
                OptDesc {
                    long: Some("strip-flow"),
                    desc: Some("Strip flow types"),
                    ..Default::default()
                },
            ),
            force_async_arrow_space: Opt::new_bool(
                cl,
                OptDesc {
                    long: Some("force-async-arrow-space"),
                    desc: Some("Force a space after the `async` keyword in arrow functions."),
                    ..Default::default()
                },
            ),
            emit_doc_block: Opt::new_bool(
                cl,
                OptDesc {
                    long: Some("emit-doc-block"),
                    desc: Some(
                        "Pass through the doc block from the original files when generating JS",
                    ),
                    ..Default::default()
                },
            ),
            double_quote_strings: Opt::new_bool(
                cl,
                OptDesc {
                    long: Some("double-quote-strings"),
                    desc: Some(
                        "When generating JS, use double quotes as the string literal delimiters",
                    ),
                    ..Default::default()
                },
            ),
            run: Opt::new_flag(
                cl,
                OptDesc {
                    long: Some("run"),
                    desc: Some("Run the parsed AST"),
                    ..Default::default()
                },
            ),
            dialect: Opt::new_enum(
                cl,
                OptDesc {
                    long: Some("dialect"),
                    desc: Some("Control the recognized JavaScript dialect (default: js)."),
                    values: Some(&[
                        ("js", ParserDialect::JavaScript, "JavaScript"),
                        ("flow", ParserDialect::Flow, "Flow"),
                        (
                            "flow-unambiguous",
                            ParserDialect::FlowUnambiguous,
                            "Flow unambiguous",
                        ),
                        (
                            "flow-detect",
                            ParserDialect::FlowDetect,
                            "Detect @flow pragma, otherwise only parse unambiguous Flow",
                        ),
                        ("ts", ParserDialect::TypeScript, "TypeScript"),
                    ]),
                    init: Some(ParserDialect::JavaScript),
                    category: input_cat,
                    ..Default::default()
                },
            ),
            jsx: Opt::new_bool(
                cl,
                OptDesc {
                    long: Some("jsx"),
                    desc: Some("Enable JSX parsing."),
                    category: input_cat,
                    ..Default::default()
                },
            ),
            strict_mode: Opt::new_bool(
                cl,
                OptDesc {
                    long: Some("strict-mode"),
                    desc: Some("Enable strict mode."),
                    category: input_cat,
                    ..Default::default()
                },
            ),
            warn_undefined: Opt::new_bool(
                cl,
                OptDesc {
                    long: Some("warn-undefined"),
                    desc: Some("Warn about undefined variables in strict mode functions."),
                    ..Default::default()
                },
            ),
            xtime: Opt::new_bool(
                cl,
                OptDesc {
                    long: Some("Xtime"),
                    desc: Some("Measure and print times."),
                    hidden: Hidden::Yes,
                    ..Default::default()
                },
            ),
            xmem: Opt::new_bool(
                cl,
                OptDesc {
                    long: Some("Xmem"),
                    desc: Some("Measure and print memory usage."),
                    hidden: Hidden::Yes,
                    ..Default::default()
                },
            ),
        }
    }

    /// Ensure the arguments are valid.
    /// Return `Err` if there are any conflicts.
    fn validate(&self) -> anyhow::Result<()> {
        if *self.sourcemap {
            ensure!(
                *self.output_path != Path::new("-"),
                "Source map requires an output path",
            );
            ensure!(*self.gen == Gen::Js, "Source map requires JS output");
        }
        Ok(())
    }
}

/// Read the specified file or stdin into a null terminated buffer.
fn read_file_or_stdin(input: &Path) -> anyhow::Result<NullTerminatedBuf> {
    if input == Path::new("-") {
        let stdin = std::io::stdin();
        let mut handle = stdin.lock();
        Ok(NullTerminatedBuf::from_reader(&mut handle).context("stdin")?)
    } else {
        let mut file = File::open(input).with_context(|| input.display().to_string())?;
        Ok(NullTerminatedBuf::from_file(&mut file).with_context(|| input.display().to_string())?)
    }
}

/// If there is a magic comment of the specified type, attempt to parse it as an URL.
fn parse_magic_url(
    parsed: &ParsedJS,
    kind: MagicCommentKind,
    opt: &Options,
) -> Result<Option<Url>, Error> {
    parsed
        .magic_comment(kind)
        .map(|s| Url::options().base_url(opt.base_url.as_ref()).parse(s))
        .transpose()
        .with_context(|| format!("Error parsing {}", kind.name()))
}

/// Consume the URL and fetch and parse the source map from it.
/// The URL is consumed to ensure that it is freed immediately after it is no
/// longer needed. Data URLs can potentially contain megabytes of data.
fn load_source_map(url: Url) -> anyhow::Result<SourceMap> {
    fetchurl::fetch_url(&url, Default::default())
        .with_context(|| format!("Source map: {}", &url))
        .and_then(|data| {
            SourceMap::from_slice(data.as_ref())
                .with_context(|| format!("Source map: {}: error parsing", &url))
        })
}

/// Convert a `Program` (script) AST node to a `Module` with an identical body.
fn script_to_module<'gc>(
    lock: &'gc ast::GCLock,
    program: &'gc ast::Program<'gc>,
) -> &'gc ast::Node<'gc> {
    ast::builder::Module::build_template(
        lock,
        ast::template::Module {
            metadata: ast::TemplateMetadata {
                phantom: Default::default(),
                range: program.metadata.range,
            },
            body: program.body,
        },
    )
}

/// Generate the specified output, if any.
/// Returns whether any output was generated.
fn gen_output(
    opt: &Options,
    ctx: &mut ast::Context,
    sem: Option<&SemContext>,
    js_module: &ParsedJSModule,
) -> anyhow::Result<bool> {
    let output_path = &*opt.output_path;
    let mut out: Box<dyn Write> = if output_path == Path::new("-") {
        Box::new(std::io::stdout())
    } else {
        Box::new(File::create(output_path).with_context(|| output_path.display().to_string())?)
    };

    let final_ast = if *opt.strip_flow {
        PassManager::strip_flow().run(ctx, js_module.ast.clone())
    } else {
        js_module.ast.clone()
    };

    let final_ast = if *opt.optimize {
        PassManager::standard().run(ctx, final_ast)
    } else {
        final_ast
    };

    if *opt.run {
        juno_eval::run(&final_ast);
        return Ok(true);
    }

    match *opt.gen {
        Gen::Ast => {
            ast::dump_json(
                out,
                ctx,
                &final_ast,
                if !*opt.pretty {
                    ast::Pretty::No
                } else {
                    ast::Pretty::Yes
                },
            )?;
            Ok(true)
        }
        Gen::Js | Gen::ResolvedJs => {
            let generated_map = gen_js::generate(
                out.deref_mut(),
                ctx,
                &final_ast,
                gen_js::Opt {
                    pretty: if *opt.pretty {
                        gen_js::Pretty::Yes
                    } else {
                        gen_js::Pretty::No
                    },
                    annotation: match sem {
                        Some(sem) if *opt.gen == Gen::ResolvedJs => gen_js::Annotation::Sem(sem),
                        _ => gen_js::Annotation::No,
                    },
                    force_async_arrow_space: *opt.force_async_arrow_space,
                    doc_block: js_module.doc_block.clone(),
                    quote: if *opt.double_quote_strings {
                        gen_js::QuoteChar::Double
                    } else {
                        gen_js::QuoteChar::Single
                    },
                },
            )?;
            if *opt.sourcemap {
                // Workaround because `PathBuf` doesn't have a way to append an extension,
                // only to replace the existing one.
                let mut path = output_path.clone().into_os_string();
                path.push(".map");
                let sourcemap_file = File::create(PathBuf::from(&path))?;
                let merged_map = match &js_module.source_map {
                    None => generated_map,
                    Some(input_map) => merge_sourcemaps(input_map, &generated_map),
                };
                merged_map.to_writer(sourcemap_file)?;
                write!(out, "\n//# sourceMappingURL={}", path.to_str().unwrap())?;
            }
            Ok(true)
        }
        Gen::Sema => {
            if let Some(sem) = sem {
                sem.dump(&ast::GCLock::new(ctx));
            }
            Ok(true)
        }
    }
}

/// TransformStatus indicates whether there were parse or validation errors
/// when processing the input.
///
/// This may be a little confusing at first - why not just use std::Result like
/// a normal person??? The key is to understand that even though parse or
/// validation errors are called "errors", they are actually legitimately
/// expected output of a compiler. They are unlike, for example, a missing file
/// or an invalid URL. They have their own UI (that may display multiple errors,
/// colorize them, etc) and don't go throw the std::error::Error flow.
enum TransformStatus {
    // Compilation completed successfully.
    Success,
    // There were parse or validation errors.
    Error,
}

/// Parsed JS file with its associated sourcemap.
struct ParsedJSModule {
    id: SourceId,
    /// AST node, may be either `Program` or `Module`.
    ast: NodeRc,
    source_map: Option<SourceMap>,
    /// Doc block for the file if it exists.
    doc_block: Option<Rc<String>>,
}

fn run(opt: &Options) -> anyhow::Result<TransformStatus> {
    opt.validate()?;

    let mut ctx = ast::Context::new();

    // Propagate flags.
    if *opt.strict_mode {
        ctx.enable_strict_mode();
    }
    ctx.warn_undefined = *opt.warn_undefined;

    // Start measuring time.
    let mut timer = Timer::new();

    // Read the input into memory.
    let input_paths = opt.input_path.values();

    let mut js_modules = HashMap::<SourceId, ParsedJSModule>::new();

    for path in input_paths {
        let input = path.as_path();
        let file_id = ctx
            .sm_mut()
            .add_source(input.display().to_string(), read_file_or_stdin(input)?);
        let buf = ctx.sm().source_buffer_rc(file_id);

        // Parse.
        let parsed = hparser::ParsedJS::parse(
            hparser::ParserFlags {
                strict_mode: ctx.strict_mode(),
                enable_jsx: *opt.jsx,
                dialect: *opt.dialect,
                store_doc_block: *opt.emit_doc_block,
            },
            &buf,
        );
        timer.mark("Parse");
        if let Some(e) = parsed.first_error() {
            ctx.sm().error(SourceRange::from_loc(file_id, e.0), e.1);
            return Ok(TransformStatus::Error);
        }

        // Extract the optional source mapping URL.
        let sm_url = if *opt.input_source_map != InputSourceMap::Ignore {
            parse_magic_url(&parsed, MagicCommentKind::SourceMappingUrl, opt)?
        } else {
            None
        };

        let ast = {
            // Convert to Juno AST.
            let lock = ast::GCLock::new(&mut ctx);
            match parsed.to_ast(&lock, file_id) {
                None => return Ok(TransformStatus::Error),
                Some(program) => {
                    if input_paths.len() > 1 {
                        NodeRc::from_node(
                            &lock,
                            script_to_module(&lock, node_cast!(ast::Node::Program, program)),
                        )
                    } else {
                        NodeRc::from_node(&lock, program)
                    }
                }
            }
        };
        let doc_block = parsed.get_doc_block().map(|s| Rc::new(s.to_string()));
        // We don't need the original parser anymore.
        drop(parsed);
        timer.mark("Cvt");

        if *opt.validate_ast {
            validate_tree(&mut ctx, &ast).with_context(|| input.display().to_string())?;
            timer.mark("Validate AST");
        }

        // Fetch and parse the source map before we generate the output.
        let source_map = sm_url.map(load_source_map).transpose()?;

        js_modules.insert(
            file_id,
            ParsedJSModule {
                id: file_id,
                ast,
                source_map,
                doc_block,
            },
        );
    }

    if js_modules.len() == 1 {
        let js_module = js_modules.into_values().next().unwrap();
        let sem = if *opt.sema {
            let lock = ast::GCLock::new(&mut ctx);
            let sem = sema::resolve_program(&lock, js_module.id, js_module.ast.node(&lock));
            if lock.sm().num_errors() != 0 || lock.sm().num_warnings() != 0 {
                eprintln!(
                    "{} error(s), {} warning(s)",
                    lock.sm().num_errors(),
                    lock.sm().num_warnings()
                );
            }
            if lock.sm().num_errors() != 0 {
                return Ok(TransformStatus::Error);
            }

            timer.mark("Sema");
            Some(sem)
        } else {
            None
        };

        // Generate output.
        if gen_output(opt, &mut ctx, sem.as_ref(), &js_module)? {
            timer.mark("Gen");
        }
    } else {
        // Show information about semantic resolution for all modules if requested.
        if *opt.sema {
            println!("{} modules", js_modules.len());
            let mut sems = Vec::new();
            let resolver = resolve_dependency::DefaultResolver::new(ctx.sm());
            for module in js_modules.into_values() {
                let sem;
                {
                    let lock = ast::GCLock::new(&mut ctx);
                    sem = sema::resolve_module(&lock, module.ast.node(&lock), module.id, &resolver);

                    let source_name = lock.sm().source_name(module.id);
                    println!("Module: {}", source_name);
                    println!(
                        "{} error(s), {} warning(s)",
                        lock.sm().num_errors(),
                        lock.sm().num_warnings()
                    );
                    if lock.sm().num_errors() != 0 {
                        return Ok(TransformStatus::Error);
                    }
                }
                // Generate output.
                if gen_output(opt, &mut ctx, Some(&sem), &module)? {
                    timer.mark("Gen");
                }
                sems.push(sem);
            }
            timer.mark("Sema");

            drop(sems);
            timer.mark("Drop Sema");
        }
    }

    // Optionally print memory usage.
    if *opt.xmem {
        println!("Context size:  {} MB", ctx.heap_size() / 1_000_000);
        println!("Storage size:  {} MB", ctx.storage_size() / 1_000_000);
        println!("# nodes:       {}", ctx.num_nodes());
    }

    // Drop the AST. We are doing it explicitly just to measure the time.
    drop(ctx);
    timer.mark("Drop");

    // Optionally print elapsed times.
    if *opt.xtime {
        print!("{:#}", timer);
    }

    Ok(TransformStatus::Success)
}

fn main() {
    let mut cl = CommandLine::new("A JavaScript compiler");
    let opt = Options::new(&mut cl);
    cl.parse_env_args();

    match run(&opt) {
        Ok(TransformStatus::Success) => {}
        Ok(TransformStatus::Error) => exit(1),
        Err(e) => {
            eprintln!("{:#}", e);
            exit(1);
        }
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

use std::env;

use assert_cmd::Command;

#[test]
fn run_lit_tests() {
    let lit = lit::lit_path();
    Command::new(lit)
        .arg("-sv")
        .arg("--param")
        .arg("test_exec_root=../../target/lit/juno")
        .arg("--param")
        .arg(format!(
            "juno={}",
            assert_cmd::cargo::cargo_bin("juno").to_str().unwrap()
        ))
        .arg("--param")
        .arg(format!("FileCheck={}", lit::filecheck_path()))
        .arg(format!("{}/../../lit/juno", env!("CARGO_MANIFEST_DIR")))
        .assert()
        .success();
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

use juno_ast::NodeRc;

use crate::eval::*;

#[allow(dead_code)]
mod eval;

pub fn run(ast: &NodeRc) {
    let _ = eval::script::script_evaluation(&mut runtime::Runtime::new(), ast);
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

use std::rc::Rc;

use super::jsvalue::*;
use super::reference::Reference;

#[derive(Debug, Clone)]
pub enum AbruptCompletion {
    Break(Option<Rc<JSString>>),
    Continue(Option<Rc<JSString>>),
    Return(JSValue),
    Throw(JSValue),
}

#[derive(Debug, Clone, PartialEq)]
pub enum NormalCompletion {
    Empty,
    Value(JSValue),
    Reference(Reference),
}

pub type CompletionRecord = Result<NormalCompletion, AbruptCompletion>;

/// https://262.ecma-international.org/11.0/#sec-updateempty
fn update_empty(cr: CompletionRecord, value: JSValue) -> CompletionRecord {
    match cr {
        Ok(NormalCompletion::Empty) => Ok(NormalCompletion::Value(value)),
        Err(AbruptCompletion::Break(None)) => Err(AbruptCompletion::Break(Some(jsvalue_cast!(
            JSValue::String,
            value
        )))),
        Err(AbruptCompletion::Continue(None)) => Err(AbruptCompletion::Continue(Some(
            jsvalue_cast!(JSValue::String, value),
        ))),
        _ => cr,
    }
}

impl NormalCompletion {
    pub fn unwrap_value(self) -> JSValue {
        if let NormalCompletion::Value(val) = self {
            return val;
        }
        panic!("Attempting to unwrap non-value.");
    }
    pub fn unwrap_reference(self) -> Reference {
        if let NormalCompletion::Reference(r) = self {
            return r;
        }
        panic!("Attempting to unwrap non-reference.");
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

use std::rc::Rc;

use super::completion_record::*;
use super::jsvalue::*;
use super::runtime::*;
use crate::eval::jsobject::PropertyDescriptor;
use crate::eval::operations::to_boolean;

#[derive(Debug)]
pub struct Binding {
    pub mutable: bool,
    pub deletable: bool,
    pub strict: bool,
    pub value: Option<JSValue>,
}

#[derive(Debug)]
pub struct DeclarativeEnv {
    pub names: Vec<Rc<JSString>>,
    pub bindings: Vec<Binding>,
}

#[derive(Debug)]
pub struct ObjectEnv {
    pub binding_object: Option<ObjectAddr>,
    pub with_environment: bool,
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ThisBindingStatus {
    Lexical,
    Initialized,
    Uninitialized,
}

#[derive(Debug)]
pub struct FunctionEnv {
    pub this_value: JSValue,
    pub this_binding_status: ThisBindingStatus,
    pub function_object: JSValue,
    pub home_object: JSValue,
    pub new_target: JSValue,
}

#[derive(Debug)]
pub struct GlobalEnv {
    pub global_this_value: JSValue,
    pub var_names: Vec<Rc<JSString>>,
}

pub struct EnvironmentMethods {
    pub has_binding: fn(&mut Runtime, EnvRecordAddr, &Rc<JSString>) -> CompletionRecord,
    pub create_mutable_binding:
        fn(&mut Runtime, EnvRecordAddr, Rc<JSString>, bool) -> CompletionRecord,
    pub create_immutable_binding:
        fn(&mut Runtime, EnvRecordAddr, Rc<JSString>, bool) -> CompletionRecord,
    pub initialize_binding:
        fn(&mut Runtime, EnvRecordAddr, &Rc<JSString>, JSValue) -> CompletionRecord,
    pub set_mutable_binding:
        fn(&mut Runtime, EnvRecordAddr, &Rc<JSString>, JSValue, bool) -> CompletionRecord,
    pub get_binding_value: fn(&mut Runtime, EnvRecordAddr, &Rc<JSString>, bool) -> CompletionRecord,
    pub get_this_binding: fn(&mut Runtime, EnvRecordAddr) -> CompletionRecord,
    pub delete_binding: fn(&mut Runtime, EnvRecordAddr, &Rc<JSString>) -> CompletionRecord,
    pub has_this_binding: fn(&Runtime, EnvRecordAddr) -> bool,
    pub has_super_binding: fn(&Runtime, EnvRecordAddr) -> bool,
    pub with_base_object: fn(&Runtime, EnvRecordAddr) -> Option<ObjectAddr>,
}

#[derive(Copy, Clone, Debug, PartialEq)]
pub enum EnvironmentRecordKind {
    /// Declarative
    Declarative,
    /// Declarative + Function
    Function,
    /// Object
    Object,
    /// Declarative + Object
    Global,
}

// declarative_record: DeclarativeEnv,
pub struct EnvironmentRecord {
    pub methods: &'static EnvironmentMethods,
    /// This field used for debugging.
    pub kind: EnvironmentRecordKind,

    pub decl: DeclarativeEnv,
    pub func: FunctionEnv,
    pub obj: ObjectEnv,
    pub glob: GlobalEnv,
}

impl EnvironmentRecord {
    pub fn new(kind: EnvironmentRecordKind) -> EnvironmentRecord {
        EnvironmentRecord {
            methods: match kind {
                EnvironmentRecordKind::Declarative => &DECL_ENV_METHODS,
                EnvironmentRecordKind::Function => &FUNCTION_ENV_METHODS,
                EnvironmentRecordKind::Object => &OBJECT_ENV_METHODS,
                EnvironmentRecordKind::Global => &GLOBAL_ENV_METHODS,
            },
            kind,
            decl: DeclarativeEnv {
                names: Vec::new(),
                bindings: Vec::new(),
            },
            func: FunctionEnv {
                this_value: JSValue::Undefined,
                this_binding_status: ThisBindingStatus::Uninitialized,
                function_object: JSValue::Undefined,
                home_object: JSValue::Undefined,
                new_target: JSValue::Undefined,
            },
            obj: ObjectEnv {
                binding_object: None,
                with_environment: false,
            },
            glob: GlobalEnv {
                global_this_value: JSValue::Undefined,
                var_names: Vec::new(),
            },
        }
    }
}

fn unreachable_get_this_binding(_run: &mut Runtime, _eaddr: EnvRecordAddr) -> CompletionRecord {
    unimplemented!()
}

static DECL_ENV_METHODS: EnvironmentMethods = EnvironmentMethods {
    has_binding: DeclarativeEnv::has_binding,
    create_mutable_binding: DeclarativeEnv::create_mutable_binding,
    create_immutable_binding: DeclarativeEnv::create_immutable_binding,
    initialize_binding: DeclarativeEnv::initialize_binding,
    set_mutable_binding: DeclarativeEnv::set_mutable_binding,
    get_binding_value: DeclarativeEnv::get_binding_value,
    get_this_binding: unreachable_get_this_binding,
    delete_binding: DeclarativeEnv::delete_binding,
    has_this_binding: DeclarativeEnv::has_this_binding,
    has_super_binding: DeclarativeEnv::has_super_binding,
    with_base_object: DeclarativeEnv::with_base_object,
};

static OBJECT_ENV_METHODS: EnvironmentMethods = EnvironmentMethods {
    has_binding: ObjectEnv::has_binding,
    create_mutable_binding: ObjectEnv::create_mutable_binding,
    create_immutable_binding: ObjectEnv::create_immutable_binding,
    initialize_binding: ObjectEnv::initialize_binding,
    set_mutable_binding: ObjectEnv::set_mutable_binding,
    get_binding_value: ObjectEnv::get_binding_value,
    get_this_binding: unreachable_get_this_binding,
    delete_binding: ObjectEnv::delete_binding,
    has_this_binding: ObjectEnv::has_this_binding,
    has_super_binding: ObjectEnv::has_super_binding,
    with_base_object: ObjectEnv::with_base_object,
};

static FUNCTION_ENV_METHODS: EnvironmentMethods = EnvironmentMethods {
    has_binding: DeclarativeEnv::has_binding,
    create_mutable_binding: DeclarativeEnv::create_mutable_binding,
    create_immutable_binding: DeclarativeEnv::create_immutable_binding,
    initialize_binding: DeclarativeEnv::initialize_binding,
    set_mutable_binding: DeclarativeEnv::set_mutable_binding,
    get_binding_value: DeclarativeEnv::get_binding_value,
    get_this_binding: FunctionEnv::get_this_binding,
    delete_binding: DeclarativeEnv::delete_binding,
    has_this_binding: FunctionEnv::has_this_binding,
    has_super_binding: FunctionEnv::has_super_binding,
    with_base_object: DeclarativeEnv::with_base_object,
};

static GLOBAL_ENV_METHODS: EnvironmentMethods = EnvironmentMethods {
    has_binding: GlobalEnv::has_binding,
    create_mutable_binding: GlobalEnv::create_mutable_binding,
    create_immutable_binding: GlobalEnv::create_immutable_binding,
    initialize_binding: GlobalEnv::initialize_binding,
    set_mutable_binding: GlobalEnv::set_mutable_binding,
    get_binding_value: GlobalEnv::get_binding_value,
    get_this_binding: GlobalEnv::get_this_binding,
    delete_binding: GlobalEnv::delete_binding,
    has_this_binding: GlobalEnv::has_this_binding,
    has_super_binding: GlobalEnv::has_super_binding,
    with_base_object: GlobalEnv::with_base_object,
};

impl DeclarativeEnv {
    fn find_binding(&self, name: &JSString) -> Option<usize> {
        self.names.iter().position(|v| **v == *name)
    }

    fn binding(&self, index: usize) -> &Binding {
        &self.bindings[index]
    }
    fn binding_mut(&mut self, index: usize) -> &mut Binding {
        &mut self.bindings[index]
    }

    fn create_binding(
        &mut self,
        name: Rc<JSString>,
        mutable: bool,
        deletable: bool,
        strict: bool,
    ) -> usize {
        debug_assert!(self.find_binding(&name).is_none());
        self.names.push(name);
        self.bindings.push(Binding {
            mutable,
            deletable,
            strict,
            value: None,
        });
        self.bindings.len() - 1
    }

    fn initialize_binding_impl(&mut self, index: usize, value: JSValue) {
        debug_assert!(
            self.bindings[index].value.is_none(),
            "binding must be uninitialized"
        );
        self.bindings[index].value = Some(value);
    }

    fn remove_binding(&mut self, index: usize) {
        self.names.remove(index);
        self.bindings.remove(index);
    }
}

//noinspection RsSelfConvention
impl DeclarativeEnv {
    /// https://262.ecma-international.org/11.0/#sec-declarative-environment-records-hasbinding-n
    fn has_binding(run: &mut Runtime, eaddr: EnvRecordAddr, n: &Rc<JSString>) -> CompletionRecord {
        Ok(NormalCompletion::Value(JSValue::Boolean(
            run.env_record(eaddr).decl.find_binding(n).is_some(),
        )))
    }

    /// https://262.ecma-international.org/11.0/#sec-declarative-environment-records-createmutablebinding-n-d
    fn create_mutable_binding(
        run: &mut Runtime,
        eaddr: EnvRecordAddr,
        name: Rc<JSString>,
        deletable: bool,
    ) -> CompletionRecord {
        run.env_record_mut(eaddr)
            .decl
            .create_binding(name, true, deletable, false);
        Ok(NormalCompletion::Value(JSValue::Boolean(true)))
    }

    /// https://262.ecma-international.org/11.0/#sec-declarative-environment-records-createimmutablebinding-n-s
    fn create_immutable_binding(
        run: &mut Runtime,
        eaddr: EnvRecordAddr,
        name: Rc<JSString>,
        strict: bool,
    ) -> CompletionRecord {
        run.env_record_mut(eaddr)
            .decl
            .create_binding(name, false, false, strict);
        Ok(NormalCompletion::Value(JSValue::Boolean(true)))
    }

    /// https://262.ecma-international.org/11.0/#sec-declarative-environment-records-initializebinding-n-v
    fn initialize_binding(
        run: &mut Runtime,
        eaddr: EnvRecordAddr,
        name: &Rc<JSString>,
        v: JSValue,
    ) -> CompletionRecord {
        let dcl_rec = &mut run.env_record_mut(eaddr).decl;
        dcl_rec.initialize_binding_impl(dcl_rec.find_binding(name).expect("binding must exist"), v);
        Ok(NormalCompletion::Empty)
    }

    /// https://262.ecma-international.org/11.0/#sec-declarative-environment-records-setmutablebinding-n-v-s
    pub fn set_mutable_binding(
        run: &mut Runtime,
        eaddr: EnvRecordAddr,
        name: &Rc<JSString>,
        value: JSValue,
        mut strict: bool,
    ) -> CompletionRecord {
        let dcl_rec = &mut run.env_record_mut(eaddr).decl;
        let bindex = dcl_rec.find_binding(name);
        if bindex.is_none() {
            if strict {
                return run.reference_error(format!("undefined variable '{}'", name));
            }
            let index = dcl_rec.create_binding(name.clone(), true, true, false);
            dcl_rec.initialize_binding_impl(index, value);
            return Ok(NormalCompletion::Empty);
        }
        let index = bindex.unwrap();
        let binding = dcl_rec.binding_mut(index);
        if binding.strict {
            strict = true;
        }
        if binding.value.is_none() {
            return run.reference_error(format!("uninitialized variable '{}'", name));
        }
        if binding.mutable {
            binding.value = Some(value);
        } else if strict {
            return run.reference_error(format!(
                "attempting to modify immutable variable '{}'",
                name
            ));
        }

        Ok(NormalCompletion::Empty)
    }

    /// https://262.ecma-international.org/11.0/#sec-declarative-environment-records-getbindingvalue-n-s
    pub fn get_binding_value(
        run: &mut Runtime,
        eaddr: EnvRecordAddr,
        name: &Rc<JSString>,
        _strict: bool,
    ) -> CompletionRecord {
        let dcl_rec = &run.env_record(eaddr).decl;
        let index = dcl_rec.find_binding(&*name).expect("binding must exist");
        let binding = dcl_rec.binding(index);
        if let Some(v) = &binding.value {
            Ok(NormalCompletion::Value(v.clone()))
        } else {
            return run.reference_error(format!("uninitialized variable '{}'", name));
        }
    }

    /// https://262.ecma-international.org/11.0/#sec-declarative-environment-records-deletebinding-n
    fn delete_binding(
        run: &mut Runtime,
        eaddr: EnvRecordAddr,
        name: &Rc<JSString>,
    ) -> CompletionRecord {
        let dcl_rec = &mut run.env_record_mut(eaddr).decl;
        let index = dcl_rec.find_binding(name).expect("binding must exist");
        if !dcl_rec.binding(index).deletable {
            return Ok(NormalCompletion::Value(JSValue::Boolean(false)));
        }
        dcl_rec.remove_binding(index);
        Ok(NormalCompletion::Value(JSValue::Boolean(true)))
    }

    /// https://262.ecma-international.org/11.0/#sec-declarative-environment-records-hasthisbinding
    fn has_this_binding(_run: &Runtime, _eaddr: EnvRecordAddr) -> bool {
        false
    }

    /// https://262.ecma-international.org/11.0/#sec-declarative-environment-records-hassuperbinding
    fn has_super_binding(_run: &Runtime, _eaddr: EnvRecordAddr) -> bool {
        false
    }

    /// https://262.ecma-international.org/11.0/#sec-declarative-environment-records-withbaseobject
    fn with_base_object(_run: &Runtime, _eaddr: EnvRecordAddr) -> Option<ObjectAddr> {
        None
    }
}

impl FunctionEnv {
    /// https://262.ecma-international.org/11.0/#sec-bindthisvalue
    fn bind_this_value(run: &mut Runtime, eaddr: EnvRecordAddr, v: JSValue) -> CompletionRecord {
        // 1. Let envRec be the function Environment Record for which the method was invoked.
        // 2. Assert: envRec.[[ThisBindingStatus]] is not lexical.
        let env_rec = run.env_record_mut(eaddr);
        // 3. If envRec.[[ThisBindingStatus]] is initialized, throw a ReferenceError exception.
        if env_rec.func.this_binding_status == ThisBindingStatus::Initialized {
            return run.reference_error("this binding is already initialized");
        }
        // 4. Set envRec.[[ThisValue]] to V.
        env_rec.func.this_value = v.clone();
        // 5. Set envRec.[[ThisBindingStatus]] to initialized.
        env_rec.func.this_binding_status = ThisBindingStatus::Initialized;
        // 6. Return V.
        Ok(NormalCompletion::Value(v))
    }

    /// https://262.ecma-international.org/11.0/#sec-getsuperbase
    fn get_super_base(run: &mut Runtime, eaddr: EnvRecordAddr) -> CompletionRecord {
        // 1. Let envRec be the function Environment Record for which the method was invoked.
        let env_rec = run.env_record(eaddr);
        // 2. Let home be envRec.[[HomeObject]].
        let home = &env_rec.func.home_object;
        match home {
            // 3. If home has the value undefined, return undefined.
            JSValue::Undefined => Ok(NormalCompletion::Value(JSValue::Undefined)),
            // 4. Assert: Type(home) is Object.
            // 5. Return ? home.[[GetPrototypeOf]]().
            JSValue::Object(obj_addr) => {
                let obj = run.object(*obj_addr);
                let proto = (obj.methods.get_prototype_of)(run, *obj_addr);
                Ok(NormalCompletion::Value(proto.clone()))
            }
            _ => unreachable!(),
        }
    }
}

impl FunctionEnv {
    /// https://262.ecma-international.org/11.0/#sec-function-environment-records-hasthisbinding
    fn has_this_binding(run: &Runtime, eaddr: EnvRecordAddr) -> bool {
        // 1. Let envRec be the function Environment Record for which the method was invoked.
        let env_rec = run.env_record(eaddr);
        // 2. If envRec.[[ThisBindingStatus]] is lexical, return false; otherwise, return true.
        env_rec.func.this_binding_status != ThisBindingStatus::Lexical
    }

    /// https://262.ecma-international.org/11.0/#sec-function-environment-records-hassuperbinding
    fn has_super_binding(run: &Runtime, eaddr: EnvRecordAddr) -> bool {
        // 1. Let envRec be the function Environment Record for which the method was invoked.
        let env_rec = run.env_record(eaddr);
        // 2. If envRec.[[ThisBindingStatus]] is lexical, return false.
        if env_rec.func.this_binding_status == ThisBindingStatus::Lexical {
            return false;
        }
        // 3. If envRec.[[HomeObject]] has the value undefined, return false; otherwise, return true.
        !matches!(env_rec.func.home_object, JSValue::Undefined)
    }

    /// https://262.ecma-international.org/11.0/#sec-function-environment-records-getthisbinding
    fn get_this_binding(run: &mut Runtime, eaddr: EnvRecordAddr) -> CompletionRecord {
        // 1. Let envRec be the function Environment Record for which the method was invoked.
        // 2. Assert: envRec.[[ThisBindingStatus]] is not lexical.
        let env_rec = run.env_record_mut(eaddr);
        // 3. If envRec.[[ThisBindingStatus]] is uninitialized, throw a ReferenceError exception.
        let binding_status = env_rec.func.this_binding_status;
        if binding_status == ThisBindingStatus::Uninitialized {
            return run.reference_error("this binding is uninitialized");
        }
        // 4. Return envRec.[[ThisValue]].
        Ok(NormalCompletion::Value(env_rec.func.this_value.clone()))
    }
}

impl ObjectEnv {
    /// https://262.ecma-international.org/11.0/#sec-object-environment-records-hasbinding-n
    fn has_binding(run: &mut Runtime, eaddr: EnvRecordAddr, n: &Rc<JSString>) -> CompletionRecord {
        let env_rec = run.env_record(eaddr);
        let nv = JSValue::String(n.clone());
        if !run.has_property(env_rec.obj.binding_object.unwrap(), &nv) {
            return Ok(NormalCompletion::Value(JSValue::Boolean(false)));
        }
        if !env_rec.obj.with_environment {
            return Ok(NormalCompletion::Value(JSValue::Boolean(true)));
        }
        let unscopables_sym = run.well_known_symbol(WellKnownSymbol::Unscopables);
        let binding_object_addr = env_rec.obj.binding_object.unwrap();
        let unscopables = run
            .get(binding_object_addr, &unscopables_sym)?
            .unwrap_value();
        if let JSValue::Object(unsc_addr) = unscopables {
            let blocked = to_boolean(&run.get(unsc_addr, &nv)?.unwrap_value());
            if blocked {
                return Ok(NormalCompletion::Value(JSValue::Boolean(false)));
            }
        }
        Ok(NormalCompletion::Value(JSValue::Boolean(true)))
    }

    /// https://262.ecma-international.org/11.0/#sec-object-environment-records-createmutablebinding-n-d
    fn create_mutable_binding(
        run: &mut Runtime,
        eaddr: EnvRecordAddr,
        name: Rc<JSString>,
        deletable: bool,
    ) -> CompletionRecord {
        let bindings_addr = run.env_record(eaddr).obj.binding_object.unwrap();
        run.define_property_or_throw(
            bindings_addr,
            &JSValue::String(name),
            &PropertyDescriptor {
                value: Some(JSValue::Undefined),
                writable: Some(true),
                enumerable: Some(true),
                configurable: Some(deletable),
                ..Default::default()
            },
        )
    }

    /// https://262.ecma-international.org/11.0/#sec-object-environment-records-createimmutablebinding-n-s
    fn create_immutable_binding(
        _run: &mut Runtime,
        _eaddr: EnvRecordAddr,
        _name: Rc<JSString>,
        _strict: bool,
    ) -> CompletionRecord {
        panic!("ObjectEnv::create_immutable_binding should never be used")
    }

    /// https://262.ecma-international.org/11.0/#sec-object-environment-records-initializebinding-n-v
    fn initialize_binding(
        run: &mut Runtime,
        eaddr: EnvRecordAddr,
        name: &Rc<JSString>,
        v: JSValue,
    ) -> CompletionRecord {
        Self::set_mutable_binding(run, eaddr, name, v, false)
    }

    /// https://262.ecma-international.org/11.0/#sec-object-environment-records-setmutablebinding-n-v-s
    //noinspection RsSelfConvention
    fn set_mutable_binding(
        run: &mut Runtime,
        eaddr: EnvRecordAddr,
        name: &Rc<JSString>,
        v: JSValue,
        strict: bool,
    ) -> CompletionRecord {
        let bindings_addr = run.env_record(eaddr).obj.binding_object.unwrap();
        run.set(bindings_addr, &JSValue::String(name.clone()), v, strict)
    }

    /// https://262.ecma-international.org/11.0/#sec-object-environment-records-getbindingvalue-n-s
    //noinspection RsSelfConvention
    fn get_binding_value(
        run: &mut Runtime,
        eaddr: EnvRecordAddr,
        name: &Rc<JSString>,
        strict: bool,
    ) -> CompletionRecord {
        let bindings_addr = run.env_record(eaddr).obj.binding_object.unwrap();
        let p = JSValue::String(name.clone());
        if !run.has_property(bindings_addr, &p) {
            if !strict {
                Ok(NormalCompletion::Value(JSValue::Undefined))
            } else {
                run.reference_error(format!("{} is not defined", &p))
            }
        } else {
            run.get(bindings_addr, &p)
        }
    }

    /// https://262.ecma-international.org/11.0/#sec-object-environment-records-deletebinding-n
    fn delete_binding(
        run: &mut Runtime,
        eaddr: EnvRecordAddr,
        name: &Rc<JSString>,
    ) -> CompletionRecord {
        let bindings_addr = run.env_record(eaddr).obj.binding_object.unwrap();
        let p = JSValue::String(name.clone());
        Ok(NormalCompletion::Value(JSValue::Boolean((run
            .object(bindings_addr)
            .methods
            .delete)(
            run,
            bindings_addr,
            &p,
        ))))
    }

    /// https://262.ecma-international.org/11.0/#sec-object-environment-records-hasthisbinding
    fn has_this_binding(_run: &Runtime, _eaddr: EnvRecordAddr) -> bool {
        false
    }

    /// https://262.ecma-international.org/11.0/#sec-object-environment-records-hassuperbinding
    fn has_super_binding(_run: &Runtime, _eaddr: EnvRecordAddr) -> bool {
        false
    }

    /// https://262.ecma-international.org/11.0/#sec-object-environment-records-withbaseobject
    fn with_base_object(run: &Runtime, eaddr: EnvRecordAddr) -> Option<ObjectAddr> {
        let env_rec = run.env_record(eaddr);
        if env_rec.obj.with_environment {
            Some(env_rec.obj.binding_object.unwrap())
        } else {
            None
        }
    }
}

impl GlobalEnv {
    /// https://262.ecma-international.org/11.0/#sec-hasvardeclaration
    fn has_var_declaration(run: &Runtime, eaddr: EnvRecordAddr, n: &Rc<JSString>) -> bool {
        // 1. Let envRec be the global Environment Record for which the method was invoked.
        let env_rec = run.env_record(eaddr);
        // 2. Let varDeclaredNames be envRec.[[VarNames]].
        // 3. If varDeclaredNames contains N, return true.
        // 4. Return false.
        env_rec.glob.var_names.contains(n)
    }

    /// https://262.ecma-international.org/11.0/#sec-haslexicaldeclaration
    fn has_lexical_declaration(
        run: &mut Runtime,
        eaddr: EnvRecordAddr,
        n: &Rc<JSString>,
    ) -> CompletionRecord {
        // 1. Let envRec be the global Environment Record for which the method was invoked.
        // 2. Let DclRec be envRec.[[DeclarativeRecord]].
        // 3. Return DclRec.HasBinding(N).
        DeclarativeEnv::has_binding(run, eaddr, n)
    }

    /// https://262.ecma-international.org/11.0/#sec-hasrestrictedglobalproperty
    fn has_restricted_global_property(
        run: &Runtime,
        eaddr: EnvRecordAddr,
        n: &Rc<JSString>,
    ) -> bool {
        // 1. Let envRec be the global Environment Record for which the method was invoked.
        let env_rec = run.env_record(eaddr);
        // 2. Let ObjRec be envRec.[[ObjectRecord]].
        // 3. Let globalObject be the binding object for ObjRec.
        let global_obj_addr = env_rec.obj.binding_object.unwrap();
        let global_obj = run.object(global_obj_addr);
        // 4. Let existingProp be ? globalObject.[[GetOwnProperty]](N).
        let prop = JSValue::String(n.clone());
        let existing_prop = (global_obj.methods.get_own_property)(run, global_obj_addr, &prop);
        #[allow(clippy::match_like_matches_macro)]
        match existing_prop {
            // 5. If existingProp is undefined, return false.
            // 6. If existingProp.[[Configurable]] is true, return false.
            None
            | Some(PropertyDescriptor {
                configurable: Some(true),
                ..
            }) => false,
            // 7. Return true.
            _ => true,
        }
    }

    /// https://262.ecma-international.org/11.0/#sec-candeclareglobalvar
    fn can_declare_global_var(run: &Runtime, eaddr: EnvRecordAddr, n: &Rc<JSString>) -> bool {
        // 1. Let envRec be the global Environment Record for which the method was invoked.
        let env_rec = run.env_record(eaddr);
        // 2. Let ObjRec be envRec.[[ObjectRecord]].
        // 3. Let globalObject be the binding object for ObjRec.
        let global_obj_addr = env_rec.obj.binding_object.unwrap();
        let prop = JSValue::String(n.clone());
        // 4. Let hasProperty be ? HasOwnProperty(globalObject, N).
        // 5. If hasProperty is true, return true.
        if run.has_own_property(global_obj_addr, &prop) {
            return true;
        }
        // 6. Return ? IsExtensible(globalObject).
        run.is_extensible(global_obj_addr)
    }

    /// https://262.ecma-international.org/11.0/#sec-candeclareglobalfunction
    fn can_declare_global_function(run: &Runtime, eaddr: EnvRecordAddr, n: &Rc<JSString>) -> bool {
        // 1. Let envRec be the global Environment Record for which the method was invoked.
        let env_rec = run.env_record(eaddr);
        // 2. Let ObjRec be envRec.[[ObjectRecord]].
        // 3. Let globalObject be the binding object for ObjRec.
        let global_obj_addr = env_rec.obj.binding_object.unwrap();
        let global_obj = run.object(global_obj_addr);
        let prop = JSValue::String(n.clone());
        // 4. Let existingProp be ? globalObject.[[GetOwnProperty]](N).
        let existing_prop = (global_obj.methods.get_own_property)(run, global_obj_addr, &prop);
        match existing_prop {
            // 5. If existingProp is undefined, return ? IsExtensible(globalObject).
            None => run.is_extensible(global_obj_addr),
            // 6. If existingProp.[[Configurable]] is true, return true.
            Some(PropertyDescriptor {
                configurable: Some(true),
                ..
            }) => true,
            // 7. If IsDataDescriptor(existingProp) is true and existingProp has attribute values { [[Writable]]: true,
            // [[Enumerable]]: true }, return true.
            Some(
                desc @ PropertyDescriptor {
                    writable: Some(true),
                    ..
                },
            ) => desc.is_data_descriptor(),
            // 8. Return false.
            _ => false,
        }
    }

    /// https://262.ecma-international.org/11.0/#sec-createglobalvarbinding
    fn create_global_var_binding(
        run: &mut Runtime,
        eaddr: EnvRecordAddr,
        n: &Rc<JSString>,
        d: bool,
    ) -> CompletionRecord {
        // 1. Let envRec be the global Environment Record for which the method was invoked.
        let env_rec = run.env_record(eaddr);
        // 2. Let ObjRec be envRec.[[ObjectRecord]].
        // 3. Let globalObject be the binding object for ObjRec.
        let global_obj_addr = env_rec.obj.binding_object.unwrap();
        let prop = JSValue::String(n.clone());
        // 4. Let hasProperty be ? HasOwnProperty(globalObject, N).
        let has_property = run.has_own_property(global_obj_addr, &prop);
        // 5. Let extensible be ? IsExtensible(globalObject).
        let extensible = run.is_extensible(global_obj_addr);
        // 6. If hasProperty is false and extensible is true, then
        // a. Perform ? ObjRec.CreateMutableBinding(N, D).
        // b. Perform ? ObjRec.InitializeBinding(N, undefined).
        if !has_property && extensible {
            ObjectEnv::create_mutable_binding(run, eaddr, n.clone(), d)?;
            ObjectEnv::initialize_binding(run, eaddr, n, JSValue::Undefined)?;
        }
        // 7. Let varDeclaredNames be envRec.[[VarNames]].
        let var_names = &mut run.env_record_mut(eaddr).glob.var_names;
        // 8. If varDeclaredNames does not contain N, then
        if !var_names.contains(n) {
            // a. Append N to varDeclaredNames.
            var_names.push(n.clone())
        }
        // 9. Return NormalCompletion(empty).
        Ok(NormalCompletion::Empty)
    }

    /// https://262.ecma-international.org/11.0/#sec-createglobalfunctionbinding
    fn create_global_function_binding(
        run: &mut Runtime,
        eaddr: EnvRecordAddr,
        n: &Rc<JSString>,
        v: &JSValue,
        d: bool,
    ) -> CompletionRecord {
        // 1. Let envRec be the global Environment Record for which the method was invoked.
        let env_rec = run.env_record(eaddr);
        // 2. Let ObjRec be envRec.[[ObjectRecord]].
        // 3. Let globalObject be the binding object for ObjRec.
        let global_obj_addr = env_rec.obj.binding_object.unwrap();
        let global_obj = run.object(global_obj_addr);
        let prop = JSValue::String(n.clone());
        // 4. Let existingProp be ? globalObject.[[GetOwnProperty]](N).
        let existing_prop = (global_obj.methods.get_own_property)(run, global_obj_addr, &prop);
        let desc = match existing_prop {
            // 5. If existingProp is undefined or existingProp.[[Configurable]] is true, then
            // a. Let desc be the PropertyDescriptor { [[Value]]: V, [[Writable]]: true, [[Enumerable]]: true, [[Configurable]]: D }.
            None
            | Some(PropertyDescriptor {
                configurable: Some(true),
                ..
            }) => PropertyDescriptor {
                value: Some(v.clone()),
                writable: Some(true),
                enumerable: Some(true),
                configurable: Some(d),
                ..Default::default()
            },
            // 6. Else,
            // a. Let desc be the PropertyDescriptor { [[Value]]: V }.
            _ => PropertyDescriptor {
                value: Some(v.clone()),
                ..Default::default()
            },
        };
        // 7. Perform ? DefinePropertyOrThrow(globalObject, N, desc).
        // 8. Record that the binding for N in ObjRec has been initialized.
        run.define_property_or_throw(global_obj_addr, &prop, &desc)?;
        // 9. Perform ? Set(globalObject, N, V, false).
        run.set(global_obj_addr, &prop, v.clone(), false)?;
        // 10. Let varDeclaredNames be envRec.[[VarNames]].
        let var_names = &mut run.env_record_mut(eaddr).glob.var_names;
        // 11. If varDeclaredNames does not contain N, then
        if !var_names.contains(n) {
            // a. Append N to varDeclaredNames.
            var_names.push(n.clone())
        }
        // 12. Return NormalCompletion(empty).
        Ok(NormalCompletion::Empty)
    }
}

impl GlobalEnv {
    /// https://262.ecma-international.org/11.0/#sec-global-environment-records-hasbinding-n
    fn has_binding(run: &mut Runtime, eaddr: EnvRecordAddr, n: &Rc<JSString>) -> CompletionRecord {
        if let NormalCompletion::Value(JSValue::Boolean(true)) =
            DeclarativeEnv::has_binding(run, eaddr, n)?
        {
            return Ok(NormalCompletion::Value(JSValue::Boolean(true)));
        }
        ObjectEnv::has_binding(run, eaddr, n)
    }

    /// https://262.ecma-international.org/11.0/#sec-global-environment-records-createmutablebinding-n-d
    fn create_mutable_binding(
        run: &mut Runtime,
        eaddr: EnvRecordAddr,
        name: Rc<JSString>,
        deletable: bool,
    ) -> CompletionRecord {
        if let NormalCompletion::Value(JSValue::Boolean(true)) =
            DeclarativeEnv::has_binding(run, eaddr, &name)?
        {
            return run.type_error(format!("{} is already defined", &name));
        }
        DeclarativeEnv::create_mutable_binding(run, eaddr, name, deletable)
    }

    /// https://262.ecma-international.org/11.0/#sec-global-environment-records-createimmutablebinding-n-s
    fn create_immutable_binding(
        run: &mut Runtime,
        eaddr: EnvRecordAddr,
        name: Rc<JSString>,
        strict: bool,
    ) -> CompletionRecord {
        if let NormalCompletion::Value(JSValue::Boolean(true)) =
            DeclarativeEnv::has_binding(run, eaddr, &name)?
        {
            return run.type_error(format!("{} is already defined", &name));
        }
        DeclarativeEnv::create_immutable_binding(run, eaddr, name, strict)
    }

    /// https://262.ecma-international.org/11.0/#sec-global-environment-records-initializebinding-n-v
    fn initialize_binding(
        run: &mut Runtime,
        eaddr: EnvRecordAddr,
        name: &Rc<JSString>,
        v: JSValue,
    ) -> CompletionRecord {
        if let NormalCompletion::Value(JSValue::Boolean(true)) =
            DeclarativeEnv::has_binding(run, eaddr, name)?
        {
            DeclarativeEnv::initialize_binding(run, eaddr, name, v)
        } else {
            ObjectEnv::initialize_binding(run, eaddr, name, v)
        }
    }

    /// https://262.ecma-international.org/11.0/#sec-global-environment-records-setmutablebinding-n-v-s
    //noinspection RsSelfConvention
    fn set_mutable_binding(
        run: &mut Runtime,
        eaddr: EnvRecordAddr,
        name: &Rc<JSString>,
        v: JSValue,
        strict: bool,
    ) -> CompletionRecord {
        if let NormalCompletion::Value(JSValue::Boolean(true)) =
            DeclarativeEnv::has_binding(run, eaddr, name)?
        {
            DeclarativeEnv::set_mutable_binding(run, eaddr, name, v, strict)
        } else {
            ObjectEnv::set_mutable_binding(run, eaddr, name, v, strict)
        }
    }

    /// https://262.ecma-international.org/11.0/#sec-global-environment-records-getbindingvalue-n-s
    fn get_binding_value(
        run: &mut Runtime,
        eaddr: EnvRecordAddr,
        name: &Rc<JSString>,
        strict: bool,
    ) -> CompletionRecord {
        if let NormalCompletion::Value(JSValue::Boolean(true)) =
            DeclarativeEnv::has_binding(run, eaddr, name)?
        {
            DeclarativeEnv::get_binding_value(run, eaddr, name, strict)
        } else {
            ObjectEnv::get_binding_value(run, eaddr, name, strict)
        }
    }

    /// https://262.ecma-international.org/11.0/#sec-global-environment-records-getthisbinding
    fn get_this_binding(run: &mut Runtime, eaddr: EnvRecordAddr) -> CompletionRecord {
        let env_rec = run.env_record(eaddr);
        Ok(NormalCompletion::Value(
            env_rec.glob.global_this_value.clone(),
        ))
    }

    /// https://262.ecma-international.org/11.0/#sec-global-environment-records-deletebinding-n
    fn delete_binding(
        run: &mut Runtime,
        eaddr: EnvRecordAddr,
        name: &Rc<JSString>,
    ) -> CompletionRecord {
        if let NormalCompletion::Value(JSValue::Boolean(true)) =
            DeclarativeEnv::has_binding(run, eaddr, name)?
        {
            return DeclarativeEnv::delete_binding(run, eaddr, name);
        }

        let global_object = run.env_record(eaddr).obj.binding_object.unwrap();
        if run.has_own_property(global_object, &JSValue::String(name.clone())) {
            let status = ObjectEnv::delete_binding(run, eaddr, name)?;
            if NormalCompletion::Value(JSValue::Boolean(true)) == status {
                let var_names = &mut run.env_record_mut(eaddr).glob.var_names;
                if let Some(pos) = var_names.iter().position(|e| *e == *name) {
                    var_names.remove(pos);
                }
            }
            return Ok(status);
        }

        Ok(NormalCompletion::Value(JSValue::Boolean(true)))
    }

    /// https://262.ecma-international.org/11.0/#sec-global-environment-records-hasthisbinding
    fn has_this_binding(_run: &Runtime, _eaddr: EnvRecordAddr) -> bool {
        true
    }

    /// https://262.ecma-international.org/11.0/#sec-global-environment-records-hassuperbinding
    fn has_super_binding(_run: &Runtime, _eaddr: EnvRecordAddr) -> bool {
        false
    }

    /// https://262.ecma-international.org/11.0/#sec-global-environment-records-withbaseobject
    fn with_base_object(_run: &Runtime, _eaddr: EnvRecordAddr) -> Option<ObjectAddr> {
        None
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

use super::runtime::*;

/// https://262.ecma-international.org/11.0/#table-22
#[derive(Debug)]
pub struct ExecutionContext {
    /// The currently executing function, or none if executing script/module.
    pub function: Option<ObjectAddr>,
    pub realm: Realm,
    pub script_or_module: Option<ScriptOrModule>,

    lex_env: LexicalEnvAddr,
    var_env: EnvRecordAddr,
}

impl ExecutionContext {
    pub fn new_script(_script_or_module: ScriptOrModule) -> Self {
        unimplemented!();
        // ExecutionContext {
        //     function: None,
        //     realm: (),
        //     script_or_module: Some(script_or_module),
        //
        //     lex_env: todo!(),
        //     var_env: todo!(),
        // }
    }
}

impl ExecutionContext {
    /// https://262.ecma-international.org/11.0/#sec-getactivescriptormodule
    /// "Return null" here will return `None`.
    pub fn get_active_script_or_module(run: &Runtime) -> Option<&ScriptOrModule> {
        // 1. If the execution context stack is empty, return null.
        if run.contexts().is_empty() {
            return None;
        }

        // 2. Let ec be the topmost execution context on the execution context stack
        //   whose ScriptOrModule component is not null.
        // 3. If no such execution context exists, return null.
        //   Otherwise, return ec's ScriptOrModule.
        run.contexts()
            .iter()
            .rev()
            .find_map(|c| c.script_or_module.as_ref())
    }

    /// https://262.ecma-international.org/11.0/#sec-getglobalobject
    pub fn get_global_object(run: &Runtime) -> ObjectAddr {
        run.global()
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

use super::completion_record::*;
use super::jsobject::*;
use super::runtime::*;

#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
pub enum ConstructorKind {
    Base,
    Derived,
}

#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
pub enum ThisMode {
    Lexical,
    Strict,
    Global,
}

pub struct FunctionMethods {
    pub call: fn() -> CompletionRecord,
    pub construct: Option<fn() -> CompletionRecord>,
}

pub struct FunctionSlots {
    pub env: EnvRecordAddr,
    pub constructor_kind: ConstructorKind,
    pub realm: Realm,
    pub this_mode: ThisMode,
    pub strict: bool,
    pub home_object: Option<ObjectAddr>,
    pub is_class_constructor: bool,
}

pub struct JSFunction {
    methods: &'static FunctionMethods,
    slots: FunctionSlots,
}

impl JSObject {
    pub fn call_impl() -> CompletionRecord {
        unimplemented!()
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

use std::cmp::Ordering;
use std::collections::HashMap;

use juno_support::declare_opaque_id;

use super::completion_record::*;
use super::function::*;
use super::operations::*;
use super::runtime::*;
use crate::eval::jsvalue::JSValue;

declare_opaque_id!(PropertyIndex);

#[derive(Clone, Debug)]
pub enum PropertyValue {
    Data {
        value: JSValue,
        writable: bool,
    },
    Accessor {
        get: Option<JSValue>,
        set: Option<JSValue>,
    },
}

#[derive(Clone, Debug)]
pub struct Property {
    value: PropertyValue,
    enumerable: bool,
    configurable: bool,
}

/// https://262.ecma-international.org/11.0/#sec-property-descriptor-specification-type
#[derive(Default, Clone)]
pub struct PropertyDescriptor {
    /// Property index populated by queries.
    pub index: Option<PropertyIndex>,

    pub value: Option<JSValue>,
    pub get: Option<JSValue>,
    pub set: Option<JSValue>,
    pub writable: Option<bool>,
    pub enumerable: Option<bool>,
    pub configurable: Option<bool>,
}

/// Internal slots for objects.
/// Sorted alphabetically for convenience.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub enum InternalSlotName {
    BooleanData,
    DateValue,
    Environment,
    ErrorData,
    Extensible,
    HomeObject,
    NumberData,
    ParameterMap,
    Prototype,
    RegExpMatcher,
    StringData,
    ThisMode,
}

#[derive(Debug)]
pub enum InternalSlotValue {
    Empty,
    Value(JSValue),
    ThisMode(ThisMode),
    LexicalEnvironment(LexicalEnvAddr),
}

impl From<JSValue> for InternalSlotValue {
    fn from(val: JSValue) -> Self {
        InternalSlotValue::Value(val)
    }
}

impl From<ThisMode> for InternalSlotValue {
    fn from(val: ThisMode) -> Self {
        InternalSlotValue::ThisMode(val)
    }
}

pub struct ObjectMethods {
    pub get_prototype_of: fn(&Runtime, ObjectAddr) -> &JSValue,
    pub set_prototype_of: fn(&mut Runtime, ObjectAddr, JSValue) -> bool,
    pub is_extensible: fn(&Runtime, ObjectAddr) -> bool,
    pub prevent_extensions: fn(&mut Runtime, ObjectAddr),
    pub get_own_property: fn(&Runtime, ObjectAddr, &JSValue) -> Option<PropertyDescriptor>,
    pub define_own_property: fn(&mut Runtime, ObjectAddr, &JSValue, &PropertyDescriptor) -> bool,
    pub has_property: fn(&Runtime, ObjectAddr, &JSValue) -> bool,
    pub get: fn(&mut Runtime, ObjectAddr, &JSValue, &JSValue) -> CompletionRecord,
    pub set: fn(&mut Runtime, ObjectAddr, &JSValue, JSValue, &JSValue) -> CompletionRecord,
    pub delete: fn(&mut Runtime, ObjectAddr, &JSValue) -> bool,
    pub own_property_keys: fn(&Runtime, ObjectAddr) -> Vec<JSValue>,
}

pub struct JSObject {
    pub methods: &'static ObjectMethods,

    keys: Vec<JSValue>,
    values: Vec<Property>,

    internal_slots: HashMap<InternalSlotName, InternalSlotValue>,

    /// If this is a function, these will be populated.
    func: Option<JSFunction>,
}

impl PropertyDescriptor {
    pub fn new(index: Option<PropertyIndex>) -> Self {
        Self {
            index,
            ..Default::default()
        }
    }

    pub fn is_empty(&self) -> bool {
        self.value.is_none()
            && self.get.is_none()
            && self.set.is_none()
            && self.writable.is_none()
            && self.enumerable.is_none()
            && self.configurable.is_none()
    }

    /// https://262.ecma-international.org/11.0/#sec-isaccessordescriptor
    pub fn is_accessor_descriptor(&self) -> bool {
        !(self.get.is_none() && self.set.is_none())
    }
    /// https://262.ecma-international.org/11.0/#sec-isdatadescriptor
    pub fn is_data_descriptor(&self) -> bool {
        !(self.value.is_none() && self.writable.is_none())
    }
    /// https://262.ecma-international.org/11.0/#sec-isgenericdescriptor
    pub fn is_generic_descriptor(&self) -> bool {
        !self.is_accessor_descriptor() && !self.is_data_descriptor()
    }
}

impl InternalSlotValue {
    pub fn get_value(&self) -> &JSValue {
        if let InternalSlotValue::Value(val) = self {
            return val;
        }
        panic!("Internal slot is not a value.");
    }
    pub fn get_lexical_environment(&self) -> LexicalEnvAddr {
        if let InternalSlotValue::LexicalEnvironment(val) = self {
            return *val;
        }
        panic!("Internal slot is not a lexical env.");
    }
}

static OBJECT_METHODS: ObjectMethods = ObjectMethods {
    /// https://262.ecma-international.org/11.0/#sec-ordinary-object-internal-methods-and-internal-slots-getprototypeof
    get_prototype_of: JSObject::ordinary_get_prototype_of,
    /// https://262.ecma-international.org/11.0/#sec-ordinary-object-internal-methods-and-internal-slots-setprototypeof-v
    set_prototype_of: JSObject::ordinary_set_prototype_of,
    /// https://262.ecma-international.org/11.0/#sec-ordinary-object-internal-methods-and-internal-slots-isextensible
    is_extensible: JSObject::ordinary_is_extensible,
    /// https://262.ecma-international.org/11.0/#sec-ordinary-object-internal-methods-and-internal-slots-preventextensions
    prevent_extensions: JSObject::ordinary_prevent_extensions,
    /// https://262.ecma-international.org/11.0/#sec-ordinary-object-internal-methods-and-internal-slots-getownproperty-p
    get_own_property: JSObject::ordinary_get_own_property,
    /// https://262.ecma-international.org/11.0/#sec-ordinary-object-internal-methods-and-internal-slots-defineownproperty-p-desc
    define_own_property: JSObject::ordinary_define_own_property,
    /// https://262.ecma-international.org/11.0/#sec-ordinary-object-internal-methods-and-internal-slots-hasproperty-p
    has_property: JSObject::ordinary_has_property,
    /// https://262.ecma-international.org/11.0/#sec-ordinary-object-internal-methods-and-internal-slots-get-p-receiver
    get: JSObject::ordinary_get,
    /// https://262.ecma-international.org/11.0/#sec-ordinary-object-internal-methods-and-internal-slots-set-p-v-receiver
    set: JSObject::ordinary_set,
    /// https://262.ecma-international.org/11.0/#sec-ordinary-object-internal-methods-and-internal-slots-delete-p
    delete: JSObject::ordinary_delete,
    /// https://262.ecma-international.org/11.0/#sec-ordinary-object-internal-methods-and-internal-slots-ownpropertykeys
    own_property_keys: JSObject::ordinary_own_property_keys,
};

impl JSObject {
    /// Make a new basic object (to be used by MakeBasicObject) with the OBJECT_METHODS methods.
    pub fn new_basic_object(internal_slots_list: &[InternalSlotName]) -> JSObject {
        let mut internal_slots = HashMap::new();
        for slot in internal_slots_list {
            internal_slots.insert(*slot, InternalSlotValue::Empty);
        }
        JSObject {
            methods: &OBJECT_METHODS,
            keys: Default::default(),
            values: Default::default(),
            internal_slots,
            func: None,
        }
    }

    pub fn function(&self) -> Option<&JSFunction> {
        self.func.as_ref()
    }
    pub fn is_function(&self) -> bool {
        self.func.is_some()
    }

    pub fn get_internal_slot(&self, slot: InternalSlotName) -> Option<&InternalSlotValue> {
        self.internal_slots.get(&slot)
    }
    pub fn set_internal_slot(&mut self, slot: InternalSlotName, value: InternalSlotValue) {
        self.internal_slots.insert(slot, value);
    }

    fn property(&self, index: PropertyIndex) -> &Property {
        &self.values[index.as_usize()]
    }
    fn property_mut(&mut self, index: PropertyIndex) -> &mut Property {
        &mut self.values[index.as_usize()]
    }

    fn prototype(&self) -> &JSValue {
        self.internal_slots
            .get(&InternalSlotName::Prototype)
            .unwrap()
            .get_value()
    }
    fn extensible(&self) -> bool {
        *jsvalue_cast!(
            JSValue::Boolean,
            self.internal_slots
                .get(&InternalSlotName::Extensible)
                .unwrap()
                .get_value()
        )
    }

    fn find_own_property(&self, p: &JSValue) -> Option<PropertyIndex> {
        self.keys
            .iter()
            .position(|k| *k == *p)
            .map(PropertyIndex::new)
    }

    fn add_property(&mut self, key: JSValue, prop: Property) -> PropertyIndex {
        self.keys.push(key);
        self.values.push(prop);
        PropertyIndex::new(self.values.len() - 1)
    }

    fn remove_property_by_index(&mut self, index: PropertyIndex) {
        self.keys.remove(index.as_usize());
        self.values.remove(index.as_usize());
    }
}

impl JSObject {
    /// https://262.ecma-international.org/11.0/#sec-ordinarygetprototypeof
    pub fn ordinary_get_prototype_of(run: &Runtime, oaddr: ObjectAddr) -> &JSValue {
        run.object(oaddr).prototype()
    }
    /// https://262.ecma-international.org/11.0/#sec-ordinarysetprototypeof
    fn ordinary_set_prototype_of(run: &mut Runtime, oaddr: ObjectAddr, value: JSValue) -> bool {
        debug_assert!(matches!(value, JSValue::Object(_) | JSValue::Null));

        let o = run.object(oaddr);
        if same_value(&value, o.prototype()) {
            return true;
        }
        if !o.extensible() {
            return false;
        }

        // Check for cycles.
        let mut p = &value;
        loop {
            match p {
                JSValue::Null => break,
                JSValue::Object(po) if *po == oaddr => {
                    // Cycle detected
                    return false;
                }
                JSValue::Object(pa) => {
                    let po = run.object(*pa);
                    if po.methods.get_prototype_of as usize
                        != Self::ordinary_get_prototype_of as usize
                    {
                        break;
                    }
                    p = po.prototype();
                }
                _ => panic!("Invalid prototype value"),
            }
        }

        run.object_mut(oaddr)
            .internal_slots
            .insert(InternalSlotName::Prototype, value.into());
        true
    }

    fn ordinary_is_extensible(run: &Runtime, oaddr: ObjectAddr) -> bool {
        run.object(oaddr).extensible()
    }

    fn ordinary_prevent_extensions(run: &mut Runtime, oaddr: ObjectAddr) {
        run.object_mut(oaddr)
            .internal_slots
            .insert(InternalSlotName::Extensible, JSValue::Boolean(false).into());
    }

    /// https://262.ecma-international.org/11.0/#sec-ordinarygetownproperty
    fn ordinary_get_own_property(
        run: &Runtime,
        oaddr: ObjectAddr,
        p: &JSValue,
    ) -> Option<PropertyDescriptor> {
        debug_assert!(is_property_key(p));
        let o = run.object(oaddr);
        match o.find_own_property(p) {
            None => None,
            Some(index) => {
                let mut d = PropertyDescriptor::new(Some(index));
                let x = &o.values[index.as_usize()];
                match x {
                    Property {
                        value: PropertyValue::Data { value, writable },
                        ..
                    } => {
                        d.value = Some(value.clone());
                        d.writable = Some(*writable);
                    }
                    Property {
                        value: PropertyValue::Accessor { get, set },
                        ..
                    } => {
                        d.get = get.clone();
                        d.set = set.clone();
                    }
                }
                d.enumerable = Some(x.enumerable);
                d.configurable = Some(x.configurable);
                Some(d)
            }
        }
    }

    fn ordinary_define_own_property(
        run: &mut Runtime,
        oaddr: ObjectAddr,
        p: &JSValue,
        desc: &PropertyDescriptor,
    ) -> bool {
        let methods = run.object(oaddr).methods;
        let current = (methods.get_own_property)(run, oaddr, p);
        let extensible = (methods.is_extensible)(run, oaddr);
        Self::validate_and_apply_property_descriptor(
            run,
            Some(oaddr),
            p,
            extensible,
            desc,
            current.as_ref(),
        )
    }

    /// https://262.ecma-international.org/11.0/#sec-validateandapplypropertydescriptor
    fn validate_and_apply_property_descriptor(
        run: &mut Runtime,
        oaddr: Option<ObjectAddr>,
        p: &JSValue,
        extensible: bool,
        desc: &PropertyDescriptor,
        current: Option<&PropertyDescriptor>,
    ) -> bool {
        debug_assert!(oaddr.is_none() || is_property_key(p));
        match current {
            None => {
                if !extensible {
                    return false;
                }
                if let Some(oaddr) = oaddr {
                    if desc.is_generic_descriptor() || desc.is_data_descriptor() {
                        run.object_mut(oaddr).add_property(
                            p.clone(),
                            Property {
                                value: PropertyValue::Data {
                                    value: desc.value.clone().unwrap_or(JSValue::Undefined),
                                    writable: desc.writable.unwrap_or(false),
                                },
                                enumerable: desc.enumerable.unwrap_or(false),
                                configurable: desc.configurable.unwrap_or(false),
                            },
                        );
                    } else {
                        run.object_mut(oaddr).add_property(
                            p.clone(),
                            Property {
                                value: PropertyValue::Accessor {
                                    get: desc.get.clone(),
                                    set: desc.set.clone(),
                                },
                                enumerable: desc.enumerable.unwrap_or(false),
                                configurable: desc.configurable.unwrap_or(false),
                            },
                        );
                    }
                }
            }
            Some(current) => {
                if desc.is_empty() {
                    return true;
                }
                if current.configurable == Some(false) {
                    if desc.configurable == Some(true) {
                        return false;
                    }
                    if desc.enumerable.is_some() && desc.enumerable != current.enumerable {
                        return false;
                    }
                }
                if !desc.is_generic_descriptor() {
                    // No further validation is required.
                } else if current.is_data_descriptor() != desc.is_data_descriptor() {
                    if current.configurable == Some(false) {
                        return false;
                    }
                    if let Some(oaddr) = oaddr {
                        if current.is_data_descriptor() {
                            run.object_mut(oaddr)
                                .property_mut(current.index.unwrap())
                                .value = PropertyValue::Accessor {
                                get: None,
                                set: None,
                            };
                        } else {
                            run.object_mut(oaddr)
                                .property_mut(current.index.unwrap())
                                .value = PropertyValue::Data {
                                value: Default::default(),
                                writable: false,
                            };
                        }
                    }
                } else if current.is_data_descriptor() && desc.is_data_descriptor() {
                    if current.configurable == Some(false) && current.writable == Some(false) {
                        if desc.writable == Some(true) {
                            return false;
                        }
                        if let (Some(old_value), Some(new_value)) = (&current.value, &desc.value) {
                            if !same_value(old_value, new_value) {
                                return false;
                            }
                        }
                        return true;
                    }
                } else {
                    debug_assert!(
                        current.is_accessor_descriptor() && desc.is_accessor_descriptor()
                    );
                    if current.configurable == Some(false) {
                        match (&desc.set, &current.set) {
                            (Some(_), None) => return false,
                            (Some(n), Some(o)) if !same_value(n, o) => return false,
                            _ => {}
                        }
                        match (&desc.get, &current.get) {
                            (Some(_), None) => return false,
                            (Some(n), Some(o)) if !same_value(n, o) => return false,
                            _ => {}
                        }
                        return true;
                    }
                }

                if let Some(oaddr) = oaddr {
                    let prop = run.object_mut(oaddr).property_mut(current.index.unwrap());
                    match &mut prop.value {
                        PropertyValue::Data { value, writable } => {
                            if let Some(v) = &desc.value {
                                *value = v.clone();
                            }
                            if let Some(v) = desc.writable {
                                *writable = v;
                            }
                        }
                        PropertyValue::Accessor { get, set } => {
                            if desc.get.is_some() {
                                *get = desc.get.clone();
                            }
                            if desc.set.is_some() {
                                *set = desc.set.clone();
                            }
                        }
                    }
                    if let Some(v) = desc.enumerable {
                        prop.enumerable = v;
                    }
                    if let Some(v) = desc.configurable {
                        prop.configurable = v;
                    }
                }
            }
        }
        true
    }

    /// https://262.ecma-international.org/11.0/#sec-ordinaryhasproperty
    fn ordinary_has_property(run: &Runtime, oaddr: ObjectAddr, p: &JSValue) -> bool {
        debug_assert!(is_property_key(p));
        let methods = run.object(oaddr).methods;
        if (methods.get_own_property)(run, oaddr, p).is_some() {
            return true;
        }
        let parent = (methods.get_prototype_of)(run, oaddr);
        if let JSValue::Object(pa) = parent {
            (methods.has_property)(run, *pa, p)
        } else {
            false
        }
    }

    /// https://262.ecma-international.org/11.0/#sec-object-environment-records-setmutablebinding-n-v-s
    fn ordinary_get(
        run: &mut Runtime,
        oaddr: ObjectAddr,
        p: &JSValue,
        receiver: &JSValue,
    ) -> CompletionRecord {
        debug_assert!(is_property_key(p));
        match (run.object(oaddr).methods.get_own_property)(run, oaddr, p) {
            None => match (run.object(oaddr).methods.get_prototype_of)(run, oaddr) {
                JSValue::Object(paddr) => {
                    // Copy to avoid the borrows checker.
                    let paddr = *paddr;
                    (run.object(paddr).methods.get)(run, paddr, p, receiver)
                }
                _ => Ok(NormalCompletion::Value(JSValue::Undefined)),
            },
            Some(desc) => {
                if desc.is_data_descriptor() {
                    Ok(NormalCompletion::Value(desc.value.unwrap()))
                } else {
                    debug_assert!(desc.is_accessor_descriptor());
                    match desc.get {
                        None => Ok(NormalCompletion::Value(JSValue::Undefined)),
                        Some(getter) => run.call(&getter, receiver, &[]),
                    }
                }
            }
        }
    }

    /// https://262.ecma-international.org/11.0/#sec-ordinaryset
    fn ordinary_set(
        run: &mut Runtime,
        oaddr: ObjectAddr,
        p: &JSValue,
        v: JSValue,
        receiver: &JSValue,
    ) -> CompletionRecord {
        debug_assert!(is_property_key(p));
        let own_desc = (run.object(oaddr).methods.get_own_property)(run, oaddr, p);
        Self::ordinary_set_with_own_descriptor(run, oaddr, p, v, receiver, own_desc.as_ref())
    }

    /// https://262.ecma-international.org/11.0/#sec-ordinarysetwithowndescriptor
    fn ordinary_set_with_own_descriptor(
        run: &mut Runtime,
        oaddr: ObjectAddr,
        p: &JSValue,
        v: JSValue,
        receiver: &JSValue,
        opt_own_desc: Option<&PropertyDescriptor>,
    ) -> CompletionRecord {
        debug_assert!(is_property_key(p));
        let own_desc = match opt_own_desc {
            None => {
                if let JSValue::Object(parent_addr) =
                    (run.object(oaddr).methods.get_prototype_of)(run, oaddr)
                {
                    // Avoid the borrows checker.
                    let parent_addr = *parent_addr;
                    return (run.object(parent_addr).methods.set)(run, parent_addr, p, v, receiver);
                }
                PropertyDescriptor {
                    value: Some(JSValue::Undefined),
                    writable: Some(true),
                    enumerable: Some(true),
                    configurable: Some(true),
                    ..Default::default()
                }
            }
            Some(d) => (*d).clone(),
        };

        if own_desc.is_data_descriptor() {
            if own_desc.writable == Some(false) {
                return Ok(NormalCompletion::Value(JSValue::Boolean(false)));
            }
            let recv_addr = match receiver {
                JSValue::Object(a) => *a,
                _ => return Ok(NormalCompletion::Value(JSValue::Boolean(false))),
            };
            let res = if let Some(existing_descriptor) =
                (run.object(recv_addr).methods.get_own_property)(run, recv_addr, p)
            {
                if existing_descriptor.is_accessor_descriptor()
                    || existing_descriptor.writable == Some(false)
                {
                    return Ok(NormalCompletion::Value(JSValue::Boolean(false)));
                }
                let value_desc = PropertyDescriptor {
                    value: Some(v),
                    ..Default::default()
                };
                (run.object(recv_addr).methods.define_own_property)(run, recv_addr, p, &value_desc)
            } else {
                run.create_data_property(recv_addr, p, v)
            };
            return Ok(NormalCompletion::Value(JSValue::Boolean(res)));
        }
        debug_assert!(own_desc.is_accessor_descriptor());
        if let Some(setter) = &own_desc.set {
            run.call(setter, receiver, &[v])
        } else {
            Ok(NormalCompletion::Value(JSValue::Boolean(false)))
        }
    }

    /// https://262.ecma-international.org/11.0/#sec-ordinarydelete
    pub fn ordinary_delete(run: &mut Runtime, oaddr: ObjectAddr, p: &JSValue) -> bool {
        debug_assert!(is_property_key(p));
        if let Some(desc) = (run.object(oaddr).methods.get_own_property)(run, oaddr, p) {
            if desc.configurable != Some(true) {
                return false;
            }
            run.object_mut(oaddr)
                .remove_property_by_index(desc.index.unwrap());
        }
        true
    }

    /// https://262.ecma-international.org/11.0/#sec-ordinaryownpropertykeys
    pub fn ordinary_own_property_keys(run: &Runtime, oaddr: ObjectAddr) -> Vec<JSValue> {
        let mut res = run.object(oaddr).keys.clone();

        // Sort: indexes first, strings next, symbols last.
        res.sort_by(|a, b| {
            let a_index = if let JSValue::String(s) = a {
                string_to_array_index(s.as_u16_slice())
            } else {
                None
            };
            let b_index = if let JSValue::String(s) = b {
                string_to_array_index(s.as_u16_slice())
            } else {
                None
            };

            match (a_index, b_index) {
                (Some(ai), Some(bi)) => ai.cmp(&bi),
                (Some(_), _) => Ordering::Less,
                (_, Some(_)) => Ordering::Greater,
                _ => match (a, b) {
                    (JSValue::String(_), JSValue::Symbol(_)) => Ordering::Less,
                    (JSValue::Symbol(_), JSValue::String(_)) => Ordering::Greater,
                    _ => Ordering::Equal,
                },
            }
        });

        res
    }

    // https://262.ecma-international.org/11.0/#sec-ordinaryobjectcreate
    pub fn ordinary_object_create(
        run: &mut Runtime,
        proto: JSValue,
        additional_internal_slots_list: Option<&[InternalSlotName]>,
    ) -> ObjectAddr {
        let mut internal_slots_list =
            vec![InternalSlotName::Prototype, InternalSlotName::Extensible];
        if let Some(additional) = additional_internal_slots_list {
            internal_slots_list.extend(additional);
        }
        let o = run.make_basic_object(&internal_slots_list);
        run.object_mut(o)
            .internal_slots
            .insert(InternalSlotName::Prototype, proto.into());
        o
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

use std::cmp::Ordering;
use std::fmt::Debug;
use std::fmt::Display;
use std::fmt::Formatter;
use std::hash::Hash;
use std::hash::Hasher;
use std::rc::Rc;

use super::runtime::*;

#[derive(Clone, Debug, PartialEq)]
pub enum JSValue {
    Undefined,
    Null,
    Boolean(bool),
    String(Rc<JSString>),
    Symbol(Rc<JSSymbol>),
    Number(f64),
    BigInt(Rc<num::BigInt>),
    Object(ObjectAddr),
}

#[macro_export]
macro_rules! jsvalue_cast {
    ($type:path, $value:expr) => {
        match $value {
            $type(v) => v,
            _ => panic!(
                "jsvalue cast to {} from {}",
                stringify!($kind),
                $value.name()
            ),
        }
    };
}

impl Default for JSValue {
    fn default() -> Self {
        JSValue::Undefined
    }
}

impl JSValue {
    pub fn cast_object(&self) -> ObjectAddr {
        *jsvalue_cast!(JSValue::Object, self)
    }
    pub fn cast_string(&self) -> &Rc<JSString> {
        jsvalue_cast!(JSValue::String, self)
    }
}

impl Display for JSValue {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        match self {
            JSValue::Undefined => write!(f, "undefined"),
            JSValue::Null => write!(f, "null"),
            JSValue::Boolean(b) => Display::fmt(b, f),
            JSValue::String(s) => write!(f, "'{}'", s),
            JSValue::Symbol(s) => write!(f, "Symbol({:p})", Rc::as_ptr(s)),
            JSValue::Number(n) => Display::fmt(n, f),
            JSValue::BigInt(b) => Display::fmt(b, f),
            JSValue::Object(o) => write!(f, "Object({:?})", *o),
        }
    }
}

impl JSValue {
    pub fn name(&self) -> &'static str {
        match self {
            JSValue::Undefined => "Undefined",
            JSValue::Null => "Null",
            JSValue::Boolean(_) => "Boolean",
            JSValue::String(_) => "String",
            JSValue::Symbol(_) => "Symbol",
            JSValue::Number(_) => "Number",
            JSValue::BigInt(_) => "BigInt",
            JSValue::Object(_) => "Object",
        }
    }
}

/// A JavaScript string. The second value is a representation for debugging.
#[derive(Clone, Debug, Eq)]
pub struct JSString(Box<[u16]>, Box<str>);

impl JSString {
    pub fn new(value: Box<[u16]>) -> Rc<Self> {
        let s = String::from_utf16_lossy(&*value);
        Rc::new(JSString(value, s.into_boxed_str()))
    }
    pub fn from_str(s: &str) -> Rc<Self> {
        let u: Vec<u16> = s.encode_utf16().collect();
        Rc::new(JSString(
            u.into_boxed_slice(),
            s.to_owned().into_boxed_str(),
        ))
    }
    pub fn len(&self) -> usize {
        self.0.len()
    }
    pub fn as_u16_slice(&self) -> &[u16] {
        self.0.as_ref()
    }
}

impl Display for JSString {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        std::fmt::Display::fmt(&self.1, f)
    }
}

impl PartialEq for JSString {
    fn eq(&self, other: &Self) -> bool {
        self.0.eq(&other.0)
    }
}

impl PartialOrd for JSString {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        self.0.partial_cmp(&other.0)
    }
}

impl Ord for JSString {
    fn cmp(&self, other: &Self) -> Ordering {
        self.0.cmp(&other.0)
    }
}

impl Hash for JSString {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.0.hash(state)
    }
}

/// A JSSymbol contains an optional description.
#[derive(Clone, Debug, Eq)]
pub struct JSSymbol(Option<Rc<JSString>>);

impl PartialEq for JSSymbol {
    fn eq(&self, other: &Self) -> bool {
        std::ptr::eq(self, other)
    }
}

impl JSSymbol {
    pub fn new_with_str(desc: &str) -> Rc<JSSymbol> {
        Rc::new(JSSymbol(Some(JSString::from_str(desc))))
    }
}