use std::cell::Cell;
use std::collections::HashMap;
use std::error::Error as StdError;
use std::fs::File;
use std::io::{BufRead, BufReader, Error as IoError, Lines};
use std::iter::Peekable;
use std::path::Path;

use eyre::{bail, eyre, Context, Result};
use lazy_static::lazy_static;
use regex::Regex;

use crate::args::Args;

use super::command_line::CommandLine;
#[cfg(feature = "full")]
use super::conditional::{Line as ConditionalLine, State as ConditionalState};
use super::inference_rules::InferenceRule;
use super::r#macro::{Set as MacroSet, Source as MacroSource};
use super::target::Target;
use super::token::{tokenize, Token, TokenString};

enum LineType {
    Rule,
    Macro,
    Unknown,
}

impl LineType {
    fn of(line_tokens: &TokenString) -> Self {
        #[cfg(feature = "full")]
        if line_tokens.starts_with("define ") {
            return Self::Macro;
        }
        for token in line_tokens.tokens() {
            if let Token::Text(text) = token {
                let colon_idx = text.find(':');
                #[cfg(not(feature = "full"))]
                let equals_idx = text.find('=');
                #[cfg(feature = "full")]
                let equals_idx = ["=", ":=", "::=", "?=", "+="]
                    .iter()
                    .filter_map(|p| text.find(p))
                    .min();
                match (colon_idx, equals_idx) {
                    (Some(_), None) => {
                        return Self::Rule;
                    }
                    (Some(c), Some(e)) if c < e => {
                        return Self::Rule;
                    }
                    (None, Some(_)) => {
                        return Self::Macro;
                    }
                    (Some(c), Some(e)) if e <= c => {
                        return Self::Macro;
                    }
                    _ => {}
                }
            }
        }
        Self::Unknown
    }
}

fn inference_match<'a>(
    targets: &[&'a str],
    prerequisites: &[String],
) -> Option<regex::Captures<'a>> {
    lazy_static! {
        static ref INFERENCE_RULE: Regex =
            Regex::new(r"^(?P<s2>(\.[^/.]+)?)(?P<s1>\.[^/.]+)$").unwrap();
        static ref SPECIAL_TARGET: Regex = Regex::new(r"^\.[A-Z]+$").unwrap();
    }

    let inference_match = INFERENCE_RULE.captures(targets[0]);
    let special_target_match = SPECIAL_TARGET.captures(targets[0]);

    let inference_rule = targets.len() == 1
        && prerequisites.is_empty()
        && inference_match.is_some()
        && special_target_match.is_none();
    if inference_rule {
        inference_match
    } else {
        None
    }
}

struct LineNumbers<T, E: StdError + Send + Sync + 'static, Inner: Iterator<Item = Result<T, E>>>(
    Inner,
    usize,
);

impl<T, E: StdError + Send + Sync + 'static, Inner: Iterator<Item = Result<T, E>>>
    LineNumbers<T, E, Inner>
{
    fn new(inner: Inner) -> Self {
        Self(inner, 0)
    }
}

impl<T, E: StdError + Send + Sync + 'static, Inner: Iterator<Item = Result<T, E>>> Iterator
    for LineNumbers<T, E, Inner>
{
    type Item = (usize, Result<T>);

    fn next(&mut self) -> Option<Self::Item> {
        self.0.next().map(|x| {
            self.1 = self.1.saturating_add(1);
            (
                self.1,
                x.with_context(|| format!("failed to read line {} of makefile", self.1)),
            )
        })
    }
}

trait IteratorExt<T, E: StdError + Send + Sync + 'static>: Iterator<Item = Result<T, E>> {
    fn line_numbered(self) -> LineNumbers<T, E, Self>
    where
        Self: Sized,
    {
        LineNumbers::new(self)
    }
}
impl<T, E: StdError + Send + Sync + 'static, I: Iterator<Item = Result<T, E>>> IteratorExt<T, E>
    for I
{
}

pub struct MakefileReader<'a, R: BufRead> {
    pub inference_rules: Vec<InferenceRule>,
    pub macros: MacroSet<'static, 'static>,
    pub targets: HashMap<String, Target>,
    pub first_non_special_target: Option<String>,
    args: &'a Args,
    lines_iter: Peekable<LineNumbers<String, IoError, Lines<R>>>,
    pending_line: Option<(usize, Result<String>)>,
    #[cfg(feature = "full")]
    conditional_stack: Vec<ConditionalState>,
}

impl<'a> MakefileReader<'a, BufReader<File>> {
    pub fn read_file(args: &'a Args, path: impl AsRef<Path>) -> Result<Self> {
        let file = File::open(path);
        // TODO handle errors
        let file = file.context("couldn't open makefile!")?;
        let file_reader = BufReader::new(file);
        Self::read(args, file_reader)
    }
}

impl<'a, R: BufRead> MakefileReader<'a, R> {
    pub fn read(args: &'a Args, source: R) -> Result<Self> {
        let mut reader = Self {
            inference_rules: Vec::new(),
            macros: MacroSet::new(),
            targets: HashMap::new(),
            first_non_special_target: None,
            args,
            lines_iter: source.lines().line_numbered().peekable(),
            #[cfg(feature = "full")]
            conditional_stack: Vec::new(),
            pending_line: None,
        };
        reader.read_all()?;
        Ok(reader)
    }

    fn read_all(&mut self) -> Result<()> {
        while let Some((line_number, line)) = self.next_line(" ") {
            let line = line?;

            // handle include lines
            if let Some(line) = line.strip_prefix("include ") {
                // remove extra leading space
                let line = line.trim_start();
                let line = self.expand_macros(&tokenize(line)?)?;
                let fields = line.split_whitespace();
                // POSIX says we only have to handle a single filename, but GNU make
                // handles arbitrarily many filenames, and it's not like that's more work
                for field in fields {
                    self.extend(MakefileReader::read_file(self.args, field)?);
                }
                continue;
            }

            if line.trim().is_empty() {
                // handle blank lines
                continue;
            }
            // unfortunately, rules vs macros can't be determined until after
            // macro tokenizing. so that's suboptimal.

            // TODO errors
            let line_tokens: TokenString = line
                .parse()
                .with_context(|| format!("failed to parse line {}", line_number))?;

            let line_type = LineType::of(&line_tokens);

            // before we actually test it, see if it's only visible after expanding macros
            let (line_tokens, line_type) = if let LineType::Unknown = line_type {
                let line_tokens = TokenString::text(self.expand_macros(&line_tokens)?);
                let line_type = LineType::of(&line_tokens);
                (line_tokens, line_type)
            } else {
                (line_tokens, line_type)
            };

            match line_type {
                LineType::Rule => self.read_rule(&line_tokens, line_number)?,
                LineType::Macro => self.read_macro(line_tokens, line_number)?,
                LineType::Unknown => {
                    if !line_tokens.is_empty() {
                        bail!(
                            "error: line {}: unknown line \"{}\"",
                            line_number,
                            line_tokens
                        );
                    }
                }
            }
        }

        Ok(())
    }

    fn next_line(
        &mut self,
        escaped_newline_replacement: &'static str,
    ) -> Option<(usize, Result<String>)> {
        if let Some(x) = self.pending_line.take() {
            return Some(x);
        }
        while let Some((line_number, line)) = self.lines_iter.next() {
            let mut line = match line {
                Ok(x) => x,
                Err(err) => return Some((line_number, Err(err))),
            };

            // handle escaped newlines
            while line.ends_with('\\') {
                line.pop();
                line.push_str(escaped_newline_replacement);
                if let Some((n, x)) = self.lines_iter.next() {
                    let x = match x {
                        Ok(x) => x,
                        Err(err) => return Some((n, Err(err))),
                    };
                    line.push_str(x.trim_start())
                }
            }

            // handle comments
            lazy_static! {
                static ref COMMENT: Regex = Regex::new("#.*$").unwrap();
            }
            let line = COMMENT.replace(&line, "").into_owned();

            #[cfg(feature = "full")]
            {
                let cond_line = ConditionalLine::from(&line, |t| self.expand_macros(t));
                let cond_line = match cond_line {
                    Ok(x) => x,
                    Err(err) => return Some((line_number, Err(err))),
                };
                if let Some(line) = cond_line {
                    let action = line.action(
                        self.conditional_stack.last(),
                        |name| self.macros.is_defined(name),
                        |t| self.expand_macros(t),
                    );
                    let action = match action {
                        Ok(x) => x,
                        Err(err) => return Some((line_number, Err(err))),
                    };
                    action.apply_to(&mut self.conditional_stack);
                    continue;
                }

                // skip lines if we need to
                if self
                    .conditional_stack
                    .last()
                    .map_or(false, ConditionalState::skipping)
                {
                    continue;
                }
            }

            return Some((line_number, Ok(line)));
        }
        None
    }

    fn next_line_if(
        &mut self,
        escaped_newline_replacement: &'static str,
        predicate: impl FnOnce(&(usize, Result<String>)) -> bool,
    ) -> Option<(usize, Result<String>)> {
        let pending_line = self.next_line(escaped_newline_replacement)?;
        if (predicate)(&pending_line) {
            Some(pending_line)
        } else {
            self.pending_line = Some(pending_line);
            None
        }
    }

    fn read_rule(&mut self, line_tokens: &TokenString, line_number: usize) -> Result<()> {
        let (targets, not_targets) = line_tokens
            .split_once(':')
            .ok_or_else(|| eyre!("read_rule couldn't find a ':' on line {}", line_number))?;
        let targets = self.expand_macros(&targets)?;
        let targets = targets.split_whitespace().collect::<Vec<_>>();
        let (prerequisites, mut commands) = match not_targets.split_once(';') {
            Some((prerequisites, command)) => {
                // TODO make sure escaped newlines get retroactively treated correctly here
                (prerequisites, vec![command])
            }
            None => (not_targets, vec![]),
        };
        let prerequisites = self.expand_macros(&prerequisites)?;
        let prerequisites = prerequisites
            .split_whitespace()
            .map(|x| x.into())
            .collect::<Vec<String>>();

        while let Some((_, x)) = self.next_line_if("\\\n", |(_, x)| {
            x.as_ref()
                .ok()
                .map_or(false, |line| line.starts_with('\t') || line.is_empty())
        }) {
            let mut line = x?;
            if !line.is_empty() {
                line.remove(0);
            }
            if line.is_empty() {
                continue;
            }
            commands.push(
                line.parse()
                    .with_context(|| format!("failed to parse line {}", line_number))?,
            );
        }

        let commands = commands
            .into_iter()
            .map(CommandLine::from)
            .collect::<Vec<_>>();

        if targets.is_empty() {
            return Ok(());
        }

        // we don't know yet if it's a target rule or an inference rule
        let inference_match = inference_match(&targets, &prerequisites);

        if let Some(inference_match) = inference_match {
            let new_rule = InferenceRule {
                product: inference_match.name("s1").unwrap().as_str().to_owned(),
                prereq: inference_match.name("s2").unwrap().as_str().to_owned(),
                commands,
            };

            self.inference_rules.retain(|existing_rule| {
                (&existing_rule.prereq, &existing_rule.product)
                    != (&new_rule.prereq, &new_rule.product)
            });
            self.inference_rules.push(new_rule);
        } else {
            for target in targets {
                if self.first_non_special_target.is_none() && !target.starts_with('.') {
                    self.first_non_special_target = Some(target.into());
                }
                match self.targets.get_mut(target) {
                    Some(old_target)
                        if commands.is_empty()
                            && !(target == ".SUFIXES" && prerequisites.is_empty()) =>
                    {
                        let new_prerequisites = prerequisites
                            .iter()
                            .filter(|x| !old_target.prerequisites.contains(x))
                            .cloned()
                            .collect::<Vec<_>>();
                        old_target.prerequisites.extend(new_prerequisites);
                    }
                    _ => {
                        let new_target = Target {
                            name: target.into(),
                            prerequisites: prerequisites.clone(),
                            commands: commands.clone(),
                            already_updated: Cell::new(false),
                        };
                        self.targets.insert(target.into(), new_target);
                    }
                }
            }
        }

        Ok(())
    }

    fn read_macro(&mut self, mut line_tokens: TokenString, line_number: usize) -> Result<()> {
        let (name, mut value) = if cfg!(feature = "full") && line_tokens.starts_with("define ") {
            line_tokens.strip_prefix("define ");
            if line_tokens.ends_with("=") {
                line_tokens.strip_suffix("=");
                line_tokens.trim_end();
            }
            let mut value = TokenString::empty();
            // TODO what should be done with escaped newlines
            while let Some((_, line)) = self.next_line(" ") {
                let line = line?;
                if line == "endef" {
                    break;
                }
                if !value.is_empty() {
                    value.extend(TokenString::text("\n"));
                }
                value.extend(line.parse()?);
            }
            (line_tokens, value)
        } else {
            line_tokens
                .split_once('=')
                .ok_or_else(|| eyre!("read_rule couldn't find a ':' on line {}", line_number))?
        };
        let name = self.expand_macros(&name)?;
        // GNUisms are annoying, but popular
        let mut expand_value = false;
        let mut skip_if_defined = false;
        let mut append = false;

        #[cfg(feature = "full")]
        let name = if let Some(real_name) = name.strip_suffix("::") {
            expand_value = true;
            real_name
        } else if let Some(real_name) = name.strip_suffix(":") {
            expand_value = true;
            real_name
        } else if let Some(real_name) = name.strip_suffix("?") {
            skip_if_defined = true;
            real_name
        } else if let Some(real_name) = name.strip_suffix("+") {
            append = true;
            real_name
        } else {
            &name
        };

        let name = name.trim_end();
        value.trim_start();

        let value = if expand_value {
            TokenString::text(self.expand_macros(&value)?)
        } else {
            value
        };

        match self.macros.get(name) {
            // We always let command line or MAKEFLAGS macros override macros from the file.
            Some((MacroSource::CommandLineOrMakeflags, _)) => return Ok(()),
            // We let environment variables override macros from the file only if the command-line argument to do that was given
            Some((MacroSource::Environment, _)) if self.args.environment_overrides => return Ok(()),
            _ if skip_if_defined => return Ok(()),
            _ => {}
        }

        let value = match self.macros.pop(name) {
            Some((_, mut old_value)) if append => {
                // TODO eagerly expand if appending to eagerly-expanded macro
                old_value.extend(TokenString::text(" "));
                old_value.extend(value);
                old_value
            }
            _ => value,
        };
        self.macros.set(name.into(), MacroSource::File, value);

        Ok(())
    }

    fn expand_macros(&self, text: &TokenString) -> Result<String> {
        self.macros.expand(text)
    }

    fn extend<R2: BufRead>(&mut self, new: MakefileReader<R2>) {
        self.inference_rules.extend(new.inference_rules);
        self.macros.extend(new.macros);
        self.targets.extend(new.targets);
        if self.first_non_special_target.is_none() {
            self.first_non_special_target = new.first_non_special_target;
        }
    }
}

#[cfg(test)]
mod test {
    use std::io::Cursor;

    use super::*;

    type R = Result<()>;

    #[cfg(feature = "full")]
    #[test]
    fn basic_conditionals() -> R {
        let file = "
ifeq (1,1)
worked = yes
else ifeq (2,2)
worked = no
else 
worked = perhaps
endif
        ";
        let args = Args::empty();
        let makefile = MakefileReader::read(&args, Cursor::new(file))?;
        assert_eq!(
            makefile.expand_macros(&TokenString::r#macro("worked"))?,
            "yes"
        );
        Ok(())
    }

    #[cfg(feature = "full")]
    #[test]
    fn define_syntax() -> R {
        let file = "
define foo =
bar
baz
endef
        ";
        let args = Args::empty();
        let makefile = MakefileReader::read(&args, Cursor::new(file))?;
        assert_eq!(
            makefile.expand_macros(&TokenString::r#macro("foo"))?,
            "bar\nbaz"
        );
        Ok(())
    }

    #[test]
    #[ignore = "I still haven't implemented `eval` or %-based macro substitution."]
    fn eval() -> R {
        // This, for the record, is a terrible misfeature.
        // If you need this, you probably shouldn't be using Make.
        // But a lot of people are using this and still use Make anyway, so here we go,
        // I guess.

        let file = "
PROGRAMS    = server client

server_OBJS = server.o server_priv.o server_access.o
server_LIBS = priv protocol

client_OBJS = client.o client_api.o client_mem.o
client_LIBS = protocol

# Everything after this is generic

.PHONY: all
all: $(PROGRAMS)

define PROGRAM_template =
 $(1): $$($(1)_OBJS) $$($(1)_LIBS:%=-l%)
 ALL_OBJS   += $$($(1)_OBJS)
endef

$(foreach prog,$(PROGRAMS),$(eval $(call PROGRAM_template,$(prog))))

$(PROGRAMS):
        $(LINK.o) $^ $(LDLIBS) -o $@

clean:
        rm -f $(ALL_OBJS) $(PROGRAMS)
        ";

        let args = Args::empty();
        let makefile = MakefileReader::read(&args, Cursor::new(file))?;
        assert!(makefile.targets.contains_key("server"));
        Ok(())
    }
}