// Proof-of-concept TAME linker
//
//  Copyright (C) 2014-2023 Ryan Specialty, LLC.
//
//  This file is part of TAME.
//
//  This program is free software: you can redistribute it and/or modify
//  it under the terms of the GNU General Public License as published by
//  the Free Software Foundation, either version 3 of the License, or
//  (at your option) any later version.
//
//  This program is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//  GNU General Public License for more details.
//
//  You should have received a copy of the GNU General Public License
//  along with this program.  If not, see <http://www.gnu.org/licenses/>.

//! **This contains the remaining portions of the proof-of-concept linker.**
//! It is feature-complete and just needs final refactoring.

use super::xmle::{
    lower::{sort, SortError},
    xir::lower_iter,
    XmleSections,
};
use crate::{
    asg::{
        air::{Air, AirAggregate, AirAggregateCtx},
        AsgError, DefaultAsg,
    },
    diagnose::{AnnotatedSpan, Diagnostic},
    fs::{
        Filesystem, FsCanonicalizer, PathFile, VisitOnceFile,
        VisitOnceFilesystem,
    },
    ld::xmle::Sections,
    obj::xmlo::{
        XmloAirContext, XmloAirError, XmloError, XmloReader, XmloToAir,
        XmloToken,
    },
    parse::{
        lowerable, FinalizeError, Lower, ParseError, Parsed, ParsedObject,
        UnknownToken,
    },
    sym::{GlobalSymbolResolve, SymbolId},
    xir::{
        flat::{PartialXirToXirf, Text, XirToXirfError, XirfToken},
        reader::XmlXirReader,
        writer::{Error as XirWriterError, XmlWriter},
        DefaultEscaper, Error as XirError, Escaper, Token as XirToken,
    },
};
use fxhash::FxBuildHasher;
use std::{
    error::Error,
    fmt::{self, Display},
    fs,
    io::{self, BufReader, BufWriter, Write},
    path::Path,
};

type LinkerAsg = DefaultAsg;

pub fn xmle(package_path: &str, output: &str) -> Result<(), TameldError> {
    let mut fs = VisitOnceFilesystem::new();
    let escaper = DefaultEscaper::default();

    let (air_ctx, state) = load_xmlo(
        package_path,
        &mut fs,
        LinkerAsg::with_capacity(65536, 65536).into(),
        &escaper,
        XmloAirContext::default(),
    )?;

    let XmloAirContext {
        prog_name: name,
        relroot,
        ..
    } = state;

    let asg = air_ctx.finish();
    let sorted = sort(&asg, Sections::new())?;

    output_xmle(
        sorted,
        name.expect("missing root package name"),
        relroot.expect("missing root package relroot"),
        output,
        &escaper,
    )?;

    Ok(())
}

fn load_xmlo<P: AsRef<Path>, S: Escaper>(
    path_str: P,
    fs: &mut VisitOnceFilesystem<FsCanonicalizer, FxBuildHasher>,
    air_ctx: AirAggregateCtx,
    escaper: &S,
    state: XmloAirContext,
) -> Result<(AirAggregateCtx, XmloAirContext), TameldError> {
    let PathFile(path, file, ctx): PathFile<BufReader<fs::File>> =
        match fs.open(path_str)? {
            VisitOnceFile::FirstVisit(file) => file,
            VisitOnceFile::Visited => return Ok((air_ctx, state)),
        };

    let src = &mut lowerable(XmlXirReader::new(file, escaper, ctx))
        .map(|result| result.map_err(TameldError::from));

    // TODO: This entire block is a WIP and will be incrementally
    //   abstracted away.
    let (mut air_ctx, mut state) = Lower::<
        ParsedObject<UnknownToken, XirToken, XirError>,
        PartialXirToXirf<4, Text>,
        _,
    >::lower(src, |toks| {
        Lower::<PartialXirToXirf<4, Text>, XmloReader, _>::lower(toks, |xmlo| {
            let mut iter = xmlo.scan(false, |st, rtok| match st {
                true => None,
                false => {
                    *st =
                        matches!(rtok, Ok(Parsed::Object(XmloToken::Eoh(..))));
                    Some(rtok)
                }
            });

            Lower::<XmloReader, XmloToAir, _>::lower_with_context(
                &mut iter,
                state,
                |air| {
                    let (_, air_ctx) =
                            Lower::<XmloToAir, AirAggregate, _>::lower_with_context(
                                air,
                                air_ctx,
                                |end| {
                                    for result in end {
                                        let _ = result?;
                                    }

                                    Ok::<_, TameldError>(())
                                },
                            )?;

                    Ok::<_, TameldError>(air_ctx)
                },
            )
        })
    })?;

    let mut dir = path;
    dir.pop();

    let found = state.found.take().unwrap_or_default();

    for relpath in found.iter() {
        let mut path_buf = dir.clone();
        path_buf.push(relpath.lookup_str());
        path_buf.set_extension("xmlo");

        (air_ctx, state) = load_xmlo(path_buf, fs, air_ctx, escaper, state)?;
    }

    Ok((air_ctx, state))
}

fn output_xmle<'a, X: XmleSections<'a>, S: Escaper>(
    sorted: X,
    name: SymbolId,
    relroot: SymbolId,
    output: &str,
    escaper: &S,
) -> Result<(), TameldError> {
    let file = fs::File::create(output)?;
    let mut buf = BufWriter::new(file);

    lower_iter(sorted, name, relroot).write(
        &mut buf,
        Default::default(),
        escaper,
    )?;
    buf.flush()?;

    Ok(())
}

// TODO: This, like everything else here, needs a home.
// TODO: Better encapsulation for `*ParseError` types.
/// Linker (`tameld`) error.
///
/// This represents the aggregation of all possible errors that can occur
///   during link-time.
/// This cannot include panics,
///   but efforts have been made to reduce panics to situations that
///   represent the equivalent of assertions.
#[derive(Debug)]
pub enum TameldError {
    Io(io::Error),
    SortError(SortError),
    XirParseError(ParseError<UnknownToken, XirError>),
    XirfParseError(ParseError<XirToken, XirToXirfError>),
    XmloParseError(ParseError<XirfToken<Text>, XmloError>),
    XmloLowerError(ParseError<XmloToken, XmloAirError>),
    AirLowerError(ParseError<Air, AsgError>),
    XirWriterError(XirWriterError),
    FinalizeError(FinalizeError),
    Fmt(fmt::Error),
}

impl From<io::Error> for TameldError {
    fn from(e: io::Error) -> Self {
        Self::Io(e)
    }
}

impl From<SortError> for TameldError {
    fn from(e: SortError) -> Self {
        Self::SortError(e)
    }
}

impl From<ParseError<UnknownToken, XirError>> for TameldError {
    fn from(e: ParseError<UnknownToken, XirError>) -> Self {
        Self::XirParseError(e)
    }
}

impl From<ParseError<XirfToken<Text>, XmloError>> for TameldError {
    fn from(e: ParseError<XirfToken<Text>, XmloError>) -> Self {
        Self::XmloParseError(e)
    }
}

impl From<ParseError<XirToken, XirToXirfError>> for TameldError {
    fn from(e: ParseError<XirToken, XirToXirfError>) -> Self {
        Self::XirfParseError(e)
    }
}

impl From<ParseError<XmloToken, XmloAirError>> for TameldError {
    fn from(e: ParseError<XmloToken, XmloAirError>) -> Self {
        Self::XmloLowerError(e)
    }
}

impl From<ParseError<Air, AsgError>> for TameldError {
    fn from(e: ParseError<Air, AsgError>) -> Self {
        Self::AirLowerError(e)
    }
}

impl From<FinalizeError> for TameldError {
    fn from(e: FinalizeError) -> Self {
        Self::FinalizeError(e)
    }
}

impl From<XirWriterError> for TameldError {
    fn from(e: XirWriterError) -> Self {
        Self::XirWriterError(e)
    }
}

impl From<fmt::Error> for TameldError {
    fn from(e: fmt::Error) -> Self {
        Self::Fmt(e)
    }
}

impl Display for TameldError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Io(e) => Display::fmt(e, f),
            Self::SortError(e) => Display::fmt(e, f),
            Self::XirParseError(e) => Display::fmt(e, f),
            Self::XirfParseError(e) => Display::fmt(e, f),
            Self::XmloParseError(e) => Display::fmt(e, f),
            Self::XmloLowerError(e) => Display::fmt(e, f),
            Self::AirLowerError(e) => Display::fmt(e, f),
            Self::XirWriterError(e) => Display::fmt(e, f),
            Self::FinalizeError(e) => Display::fmt(e, f),
            Self::Fmt(e) => Display::fmt(e, f),
        }
    }
}

impl Error for TameldError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            Self::Io(e) => Some(e),
            Self::SortError(e) => Some(e),
            Self::XirParseError(e) => Some(e),
            Self::XirfParseError(e) => Some(e),
            Self::XmloParseError(e) => Some(e),
            Self::XmloLowerError(e) => Some(e),
            Self::AirLowerError(e) => Some(e),
            Self::XirWriterError(e) => Some(e),
            Self::FinalizeError(e) => Some(e),
            Self::Fmt(e) => Some(e),
        }
    }
}

impl Diagnostic for TameldError {
    fn describe(&self) -> Vec<AnnotatedSpan> {
        match self {
            Self::XirParseError(e) => e.describe(),
            Self::XirfParseError(e) => e.describe(),
            Self::XmloParseError(e) => e.describe(),
            Self::XmloLowerError(e) => e.describe(),
            Self::AirLowerError(e) => e.describe(),
            Self::FinalizeError(e) => e.describe(),
            Self::SortError(e) => e.describe(),

            Self::Io(_) | Self::XirWriterError(_) | Self::Fmt(_) => vec![],
        }
    }
}
