use std::fmt::Write;

use serde::{Deserialize, Serialize};
use std::rc::Rc;
use tvix_eval::observer::TracingObserver;
use tvix_eval::observer::{DisassemblingObserver, NoOpObserver};
use web_sys::HtmlInputElement;
use web_sys::HtmlTextAreaElement;
use yew::prelude::*;
use yew::TargetCast;
use yew_router::{prelude::*, AnyRoute};

enum Msg {
    CodeChange(String),
    ToggleTrace(bool),
}

#[derive(Clone, Serialize, Deserialize)]
struct Model {
    code: String,
    trace: bool,
}

fn tvixbolt_overview() -> Html {
    html! {
        <>
          <p>
            {"This page lets you explore the bytecode generated by the "}
            <a href="https://cs.tvl.fyi/depot/-/tree/tvix">{"Tvix"}</a>
            {" compiler for the Nix language. See the "}
            <a href="https://tvl.fyi/blog/rewriting-nix">{"Tvix announcement"}</a>
            {" for some background information on Tvix itself."}
          </p>
          <p>
            {"Tvix is still "}<i>{"extremely work-in-progress"}</i>{" and you "}
            {"should expect to be able to cause bugs and errors in this tool."}
          </p>
        </>
    }
}

/// This renders an ad in the Tvixbolt footer. Most people that end up
/// on Tvixbolt will probably block this anyways, but might as well.
fn ad() -> Html {
    let ad_code = r#"
window.yaContextCb.push(()=>{
  Ya.Context.AdvManager.render({
    renderTo: 'yandex_rtb_R-A-1943274-1',
    blockId: 'R-A-1943274-1'
  })
})
"#;

    html! {
        <div id="ad">
            <div id="yandex_rtb_R-A-1943274-1"></div>
            <script>{ad_code}</script>
        </div>
    }
}

fn footer_link(location: &'static str, name: &str) -> Html {
    html! {
        <>
            <a class="uncoloured-link" href={location}>{name}</a>{" | "}
        </>
    }
}

fn footer() -> Html {
    html! {
        <>
        <hr/>
        <footer>
          <p class="footer">
            {footer_link("https://tvl.su", "home")}
            {footer_link("https://cs.tvl.fyi", "code")}
            {footer_link("https://tvl.fyi/builds", "ci")}
            {footer_link("https://b.tvl.fyi", "bugs")}
            {"© ООО ТВЛ"}
          </p>
          <p class="lod">{"ಠ_ಠ"}</p>
          {ad()}
        </footer>
        </>
    }
}

impl Component for Model {
    type Message = Msg;
    type Properties = ();

    fn create(_: &Context<Self>) -> Self {
        BrowserHistory::new()
            .location()
            .query::<Self>()
            .unwrap_or_else(|_| Self {
                code: String::new(),
                trace: false,
            })
    }

    fn update(&mut self, _: &Context<Self>, msg: Self::Message) -> bool {
        match msg {
            Msg::ToggleTrace(trace) => {
                self.trace = trace;
            }

            Msg::CodeChange(new_code) => {
                self.code = new_code;
            }
        }

        let _ = BrowserHistory::new().replace_with_query(AnyRoute::new("/"), self.clone());

        true
    }

    fn view(&self, ctx: &Context<Self>) -> Html {
        // This gives us a component's "`Scope`" which allows us to send messages, etc to the component.
        let link = ctx.link();
        html! {
            <>
            <div class="container">
                <h1>{"tvixbolt 0.1-alpha"}</h1>
                {tvixbolt_overview()}
                <form>
                  <fieldset>
                    <legend>{"Input"}</legend>

                    <div class="form-group">
                        <label for="code">{"Nix code:"}</label>
                        <textarea
                         oninput={link.callback(|e: InputEvent| {
                             let ta = e.target_unchecked_into::<HtmlTextAreaElement>().value();
                             Msg::CodeChange(ta)

                         })}
                         id="code" cols="30" rows="10" value={self.code.clone()}>
                         </textarea>
                    </div>

                    <div class="form-group">
                      <label for="trace-runtime">{"Trace runtime:"}</label>
                      <input
                       id="trace-runtime" type="checkbox" checked={self.trace}
                       onchange={link.callback(|e: Event| {
                           let trace = e.target_unchecked_into::<HtmlInputElement>().checked();
                           Msg::ToggleTrace(trace)
                       })}
                       />
                    </div>
                  </fieldset>
                </form>
                <hr />
                {self.run()}
                {footer()}
            </div>
            </>
        }
    }
}

impl Model {
    fn run(&self) -> Html {
        if self.code.is_empty() {
            return html! {
                <p>
                  {"Enter some Nix code above to get started. Don't know Nix yet? "}
                  {"Check out "}
                  <a href="https://code.tvl.fyi/about/nix/nix-1p/README.md">{"nix-1p"}</a>
                  {"!"}
                </p>
            };
        }

        html! {
            <>
              <h2>{"Result:"}</h2>
              {eval(self.trace, &self.code).display()}
            </>
        }
    }
}

#[derive(Default)]
struct Output {
    parse_errors: String,
    warnings: String,
    compiler_errors: String,
    runtime_errors: String,
    output: String,
    bytecode: Vec<u8>,
    trace: Vec<u8>,
}

fn maybe_show(title: &str, s: &str) -> Html {
    if s.is_empty() {
        html! {}
    } else {
        html! {
            <>
              <h3>{title}</h3>
              <pre>{s}</pre>
            </>
        }
    }
}

impl Output {
    fn display(self) -> Html {
        html! {
            <>
            {maybe_show("Parse errors:", &self.parse_errors)}
            {maybe_show("Warnings:", &self.warnings)}
            {maybe_show("Output:", &self.output)}
            {maybe_show("Compiler errors:", &self.compiler_errors)}
            {maybe_show("Bytecode:", &String::from_utf8_lossy(&self.bytecode))}
            {maybe_show("Runtime errors:", &self.runtime_errors)}
            {maybe_show("Runtime trace:", &String::from_utf8_lossy(&self.trace))}
            </>
        }
    }
}

fn eval(trace: bool, code: &str) -> Output {
    let mut out = Output::default();

    if code.is_empty() {
        return out;
    }

    let mut codemap = codemap::CodeMap::new();
    let file = codemap.add_file("nixbolt".to_string(), code.into());

    let parsed = rnix::ast::Root::parse(code);
    let errors = parsed.errors();

    if !errors.is_empty() {
        for err in errors {
            writeln!(&mut out.parse_errors, "parse error: {}", err).unwrap();
        }

        return out;
    }

    // If we've reached this point, there are no errors.
    let root_expr = parsed
        .tree()
        .expr()
        .expect("expression should exist if no errors occured");

    let codemap = Rc::new(codemap);
    let mut compilation_observer = DisassemblingObserver::new(codemap.clone(), &mut out.bytecode);

    let result = tvix_eval::compile(
        root_expr,
        Some("/nixbolt".into()),
        file.clone(),
        tvix_eval::global_builtins(),
        &mut compilation_observer,
    )
    .unwrap();

    for warning in result.warnings {
        writeln!(
            &mut out.warnings,
            "{}\n",
            warning.fancy_format_str(&codemap).trim(),
        )
        .unwrap();
    }

    if !result.errors.is_empty() {
        for error in &result.errors {
            writeln!(
                &mut out.compiler_errors,
                "{}\n",
                error.fancy_format_str(&codemap).trim(),
            )
            .unwrap();
        }

        return out;
    }

    let result = if trace {
        tvix_eval::run_lambda(&mut TracingObserver::new(&mut out.trace), result.lambda)
    } else {
        tvix_eval::run_lambda(&mut NoOpObserver::default(), result.lambda)
    };

    match result {
        Ok(value) => writeln!(&mut out.output, "{}", value).unwrap(),
        Err(err) => writeln!(
            &mut out.runtime_errors,
            "{}",
            err.fancy_format_str(&codemap).trim()
        )
        .unwrap(),
    };

    out
}

fn main() {
    yew::start_app::<Model>();
}
