use crate::common::*;

use std::process::{ExitStatus, Stdio};

/// Return a `RuntimeError::Signal` if the process was terminated by a signal,
/// otherwise return an `RuntimeError::UnknownFailure`
fn error_from_signal(
  recipe: &str,
  line_number: Option<usize>,
  exit_status: ExitStatus,
) -> RuntimeError {
  match Platform::signal_from_exit_status(exit_status) {
    Some(signal) => RuntimeError::Signal {
      recipe,
      line_number,
      signal,
    },
    None => RuntimeError::Unknown {
      recipe,
      line_number,
    },
  }
}

/// A recipe, e.g. `foo: bar baz`
#[derive(PartialEq, Debug)]
pub(crate) struct Recipe<'src, D = Dependency<'src>> {
  pub(crate) dependencies: Vec<D>,
  pub(crate) doc:          Option<&'src str>,
  pub(crate) body:         Vec<Line<'src>>,
  pub(crate) name:         Name<'src>,
  pub(crate) parameters:   Vec<Parameter<'src>>,
  pub(crate) private:      bool,
  pub(crate) quiet:        bool,
  pub(crate) shebang:      bool,
}

impl<'src, D> Recipe<'src, D> {
  pub(crate) fn argument_range(&self) -> RangeInclusive<usize> {
    self.min_arguments()..=self.max_arguments()
  }

  pub(crate) fn min_arguments(&self) -> usize {
    self
      .parameters
      .iter()
      .filter(|p| p.default.is_none() && p.kind != ParameterKind::Star)
      .count()
  }

  pub(crate) fn max_arguments(&self) -> usize {
    if self.parameters.iter().any(|p| p.kind.is_variadic()) {
      usize::max_value() - 1
    } else {
      self.parameters.len()
    }
  }

  pub(crate) fn name(&self) -> &'src str {
    self.name.lexeme()
  }

  pub(crate) fn line_number(&self) -> usize {
    self.name.line
  }

  pub(crate) fn public(&self) -> bool {
    !self.private
  }

  pub(crate) fn run<'run>(
    &self,
    context: &RecipeContext<'src, 'run>,
    dotenv: &BTreeMap<String, String>,
    scope: Scope<'src, 'run>,
    search: &'run Search,
  ) -> RunResult<'src, ()> {
    let config = &context.config;

    if config.verbosity.loquacious() {
      let color = config.color.stderr().banner();
      eprintln!(
        "{}===> Running recipe `{}`...{}",
        color.prefix(),
        self.name,
        color.suffix()
      );
    }

    let mut evaluator =
      Evaluator::recipe_evaluator(context.config, dotenv, &scope, context.settings, search);

    if self.shebang {
      let mut evaluated_lines = vec![];
      for line in &self.body {
        evaluated_lines.push(evaluator.evaluate_line(line, false)?);
      }

      if config.verbosity.loud() && (config.dry_run || self.quiet) {
        for line in &evaluated_lines {
          eprintln!("{}", line);
        }
      }

      if config.dry_run {
        return Ok(());
      }

      let tmp = tempfile::Builder::new()
        .prefix("just")
        .tempdir()
        .map_err(|error| RuntimeError::TmpdirIoError {
          recipe:   self.name(),
          io_error: error,
        })?;
      let mut path = tmp.path().to_path_buf();
      path.push(self.name());
      {
        let mut f = fs::File::create(&path).map_err(|error| RuntimeError::TmpdirIoError {
          recipe:   self.name(),
          io_error: error,
        })?;
        let mut text = String::new();
        // add the shebang
        text += &evaluated_lines[0];
        text += "\n";
        // add blank lines so that lines in the generated script have the same line
        // number as the corresponding lines in the justfile
        for _ in 1..(self.line_number() + 2) {
          text += "\n"
        }
        for line in &evaluated_lines[1..] {
          text += line;
          text += "\n";
        }

        if config.verbosity.grandiloquent() {
          eprintln!("{}", config.color.doc().stderr().paint(&text));
        }

        f.write_all(text.as_bytes())
          .map_err(|error| RuntimeError::TmpdirIoError {
            recipe:   self.name(),
            io_error: error,
          })?;
      }

      // make the script executable
      Platform::set_execute_permission(&path).map_err(|error| RuntimeError::TmpdirIoError {
        recipe:   self.name(),
        io_error: error,
      })?;

      let shebang_line = evaluated_lines
        .first()
        .ok_or_else(|| RuntimeError::Internal {
          message: "evaluated_lines was empty".to_owned(),
        })?;

      let Shebang {
        interpreter,
        argument,
      } = Shebang::new(shebang_line).ok_or_else(|| RuntimeError::Internal {
        message: format!("bad shebang line: {}", shebang_line),
      })?;

      // create a command to run the script
      let mut command = Platform::make_shebang_command(
        &path,
        &context.search.working_directory,
        interpreter,
        argument,
      )
      .map_err(|output_error| RuntimeError::Cygpath {
        recipe: self.name(),
        output_error,
      })?;

      command.export(context.settings, dotenv, &scope);

      // run it!
      match InterruptHandler::guard(|| command.status()) {
        Ok(exit_status) =>
          if let Some(code) = exit_status.code() {
            if code != 0 {
              return Err(RuntimeError::Code {
                recipe: self.name(),
                line_number: None,
                code,
              });
            }
          } else {
            return Err(error_from_signal(self.name(), None, exit_status));
          },
        Err(io_error) => {
          return Err(RuntimeError::Shebang {
            recipe: self.name(),
            command: interpreter.to_owned(),
            argument: argument.map(String::from),
            io_error,
          });
        },
      };
    } else {
      let mut lines = self.body.iter().peekable();
      let mut line_number = self.line_number() + 1;
      loop {
        if lines.peek().is_none() {
          break;
        }
        let mut evaluated = String::new();
        let mut continued = false;
        let quiet_command = lines.peek().map(|line| line.is_quiet()).unwrap_or(false);
        let infallable_command = lines
          .peek()
          .map(|line| line.is_infallable())
          .unwrap_or(false);
        loop {
          if lines.peek().is_none() {
            break;
          }
          let line = lines.next().unwrap();
          line_number += 1;
          evaluated += &evaluator.evaluate_line(line, continued)?;
          if line.is_continuation() {
            continued = true;
            evaluated.pop();
          } else {
            break;
          }
        }
        let mut command = evaluated.as_str();
        if quiet_command || infallable_command {
          command = &command[1..];
        }

        if command.is_empty() {
          continue;
        }

        if config.dry_run
          || config.verbosity.loquacious()
          || !((quiet_command ^ self.quiet) || config.verbosity.quiet())
        {
          let color = if config.highlight {
            config.color.command()
          } else {
            config.color
          };
          eprintln!("{}", color.stderr().paint(command));
        }

        if config.dry_run {
          continue;
        }

        let mut cmd = context.settings.shell_command(config);

        cmd.current_dir(&context.search.working_directory);

        cmd.arg(command);

        if config.verbosity.quiet() {
          cmd.stderr(Stdio::null());
          cmd.stdout(Stdio::null());
        }

        cmd.export(context.settings, dotenv, &scope);

        match InterruptHandler::guard(|| cmd.status()) {
          Ok(exit_status) =>
            if let Some(code) = exit_status.code() {
              if code != 0 && !infallable_command {
                return Err(RuntimeError::Code {
                  recipe: self.name(),
                  line_number: Some(line_number),
                  code,
                });
              }
            } else {
              return Err(error_from_signal(
                self.name(),
                Some(line_number),
                exit_status,
              ));
            },
          Err(io_error) => {
            return Err(RuntimeError::IoError {
              recipe: self.name(),
              io_error,
            });
          },
        };
      }
    }
    Ok(())
  }
}

impl<'src, D> Keyed<'src> for Recipe<'src, D> {
  fn key(&self) -> &'src str {
    self.name.lexeme()
  }
}

impl<'src> Display for Recipe<'src> {
  fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
    if let Some(doc) = self.doc {
      writeln!(f, "# {}", doc)?;
    }

    if self.quiet {
      write!(f, "@{}", self.name)?;
    } else {
      write!(f, "{}", self.name)?;
    }

    for parameter in &self.parameters {
      write!(f, " {}", parameter)?;
    }
    write!(f, ":")?;
    for dependency in &self.dependencies {
      write!(f, " {}", dependency)?;
    }

    for (i, line) in self.body.iter().enumerate() {
      if i == 0 {
        writeln!(f)?;
      }
      for (j, fragment) in line.fragments.iter().enumerate() {
        if j == 0 {
          write!(f, "    ")?;
        }
        match fragment {
          Fragment::Text { token } => write!(f, "{}", token.lexeme())?,
          Fragment::Interpolation { expression, .. } => write!(f, "{{{{{}}}}}", expression)?,
        }
      }
      if i + 1 < self.body.len() {
        writeln!(f)?;
      }
    }
    Ok(())
  }
}
