use crate::common::*;

#[derive(Debug)]
pub(crate) enum RuntimeError<'src> {
  ArgumentCountMismatch {
    recipe:     &'src str,
    parameters: Vec<&'src Parameter<'src>>,
    found:      usize,
    min:        usize,
    max:        usize,
  },
  Backtick {
    token:        Token<'src>,
    output_error: OutputError,
  },
  Code {
    recipe:      &'src str,
    line_number: Option<usize>,
    code:        i32,
  },
  Cygpath {
    recipe:       &'src str,
    output_error: OutputError,
  },
  Dotenv {
    dotenv_error: dotenv::Error,
  },
  FunctionCall {
    function: Name<'src>,
    message:  String,
  },
  Internal {
    message: String,
  },
  IoError {
    recipe:   &'src str,
    io_error: io::Error,
  },
  Shebang {
    recipe:   &'src str,
    command:  String,
    argument: Option<String>,
    io_error: io::Error,
  },
  Signal {
    recipe:      &'src str,
    line_number: Option<usize>,
    signal:      i32,
  },
  TmpdirIoError {
    recipe:   &'src str,
    io_error: io::Error,
  },
  UnknownOverrides {
    overrides: Vec<&'src str>,
  },
  UnknownRecipes {
    recipes:    Vec<&'src str>,
    suggestion: Option<Suggestion<'src>>,
  },
  Unknown {
    recipe:      &'src str,
    line_number: Option<usize>,
  },
  NoRecipes,
  DefaultRecipeRequiresArguments {
    recipe:        &'src str,
    min_arguments: usize,
  },
}

impl<'src> Error for RuntimeError<'src> {
  fn code(&self) -> i32 {
    match *self {
      Self::Code { code, .. }
      | Self::Backtick {
        output_error: OutputError::Code(code),
        ..
      } => code,
      _ => EXIT_FAILURE,
    }
  }
}

impl<'src> RuntimeError<'src> {
  fn context(&self) -> Option<Token> {
    use RuntimeError::*;
    match self {
      FunctionCall { function, .. } => Some(function.token()),
      Backtick { token, .. } => Some(*token),
      _ => None,
    }
  }
}

impl<'src> Display for RuntimeError<'src> {
  fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
    use RuntimeError::*;

    let color = if f.alternate() {
      Color::always()
    } else {
      Color::never()
    };
    let message = color.message();
    write!(f, "{}", message.prefix())?;

    match self {
      UnknownRecipes {
        recipes,
        suggestion,
      } => {
        write!(
          f,
          "Justfile does not contain {} {}.",
          Count("recipe", recipes.len()),
          List::or_ticked(recipes),
        )?;
        if let Some(suggestion) = *suggestion {
          write!(f, "\n{}", suggestion)?;
        }
      },
      UnknownOverrides { overrides } => {
        write!(
          f,
          "{} {} overridden on the command line but not present in justfile",
          Count("Variable", overrides.len()),
          List::and_ticked(overrides),
        )?;
      },
      ArgumentCountMismatch {
        recipe,
        parameters,
        found,
        min,
        max,
      } => {
        if min == max {
          let expected = min;
          write!(
            f,
            "Recipe `{}` got {} {} but {}takes {}",
            recipe,
            found,
            Count("argument", *found),
            if expected < found { "only " } else { "" },
            expected
          )?;
        } else if found < min {
          write!(
            f,
            "Recipe `{}` got {} {} but takes at least {}",
            recipe,
            found,
            Count("argument", *found),
            min
          )?;
        } else if found > max {
          write!(
            f,
            "Recipe `{}` got {} {} but takes at most {}",
            recipe,
            found,
            Count("argument", *found),
            max
          )?;
        }
        write!(f, "\nusage:\n    just {}", recipe)?;
        for param in parameters {
          if color.stderr().active() {
            write!(f, " {:#}", param)?;
          } else {
            write!(f, " {}", param)?;
          }
        }
      },
      Code {
        recipe,
        line_number,
        code,
      } =>
        if let Some(n) = line_number {
          write!(
            f,
            "Recipe `{}` failed on line {} with exit code {}",
            recipe, n, code
          )?;
        } else {
          write!(f, "Recipe `{}` failed with exit code {}", recipe, code)?;
        },
      Cygpath {
        recipe,
        output_error,
      } => match output_error {
        OutputError::Code(code) => {
          write!(
            f,
            "Cygpath failed with exit code {} while translating recipe `{}` shebang interpreter \
             path",
            code, recipe
          )?;
        },
        OutputError::Signal(signal) => {
          write!(
            f,
            "Cygpath terminated by signal {} while translating recipe `{}` shebang interpreter \
             path",
            signal, recipe
          )?;
        },
        OutputError::Unknown => {
          write!(
            f,
            "Cygpath experienced an unknown failure while translating recipe `{}` shebang \
             interpreter path",
            recipe
          )?;
        },
        OutputError::Io(io_error) => {
          match io_error.kind() {
            io::ErrorKind::NotFound => write!(
              f,
              "Could not find `cygpath` executable to translate recipe `{}` shebang interpreter \
               path:\n{}",
              recipe, io_error
            ),
            io::ErrorKind::PermissionDenied => write!(
              f,
              "Could not run `cygpath` executable to translate recipe `{}` shebang interpreter \
               path:\n{}",
              recipe, io_error
            ),
            _ => write!(f, "Could not run `cygpath` executable:\n{}", io_error),
          }?;
        },
        OutputError::Utf8(utf8_error) => {
          write!(
            f,
            "Cygpath successfully translated recipe `{}` shebang interpreter path, but output was \
             not utf8: {}",
            recipe, utf8_error
          )?;
        },
      },
      Dotenv { dotenv_error } => {
        writeln!(f, "Failed to load .env: {}", dotenv_error)?;
      },
      FunctionCall { function, message } => {
        writeln!(
          f,
          "Call to function `{}` failed: {}",
          function.lexeme(),
          message
        )?;
      },
      Shebang {
        recipe,
        command,
        argument,
        io_error,
      } =>
        if let Some(argument) = argument {
          write!(
            f,
            "Recipe `{}` with shebang `#!{} {}` execution error: {}",
            recipe, command, argument, io_error
          )?;
        } else {
          write!(
            f,
            "Recipe `{}` with shebang `#!{}` execution error: {}",
            recipe, command, io_error
          )?;
        },
      Signal {
        recipe,
        line_number,
        signal,
      } =>
        if let Some(n) = line_number {
          write!(
            f,
            "Recipe `{}` was terminated on line {} by signal {}",
            recipe, n, signal
          )?;
        } else {
          write!(f, "Recipe `{}` was terminated by signal {}", recipe, signal)?;
        },
      Unknown {
        recipe,
        line_number,
      } =>
        if let Some(n) = line_number {
          write!(
            f,
            "Recipe `{}` failed on line {} for an unknown reason",
            recipe, n
          )?;
        } else {
          write!(f, "Recipe `{}` failed for an unknown reason", recipe)?;
        },
      IoError { recipe, io_error } => {
        match io_error.kind() {
          io::ErrorKind::NotFound => writeln!(
            f,
            "Recipe `{}` could not be run because just could not find `sh`:{}",
            recipe, io_error
          ),
          io::ErrorKind::PermissionDenied => writeln!(
            f,
            "Recipe `{}` could not be run because just could not run `sh`:{}",
            recipe, io_error
          ),
          _ => writeln!(
            f,
            "Recipe `{}` could not be run because of an IO error while launching `sh`:{}",
            recipe, io_error
          ),
        }?;
      },
      TmpdirIoError { recipe, io_error } => writeln!(
        f,
        "Recipe `{}` could not be run because of an IO error while trying to create a temporary \
         directory or write a file to that directory`:{}",
        recipe, io_error
      )?,
      Backtick { output_error, .. } => match output_error {
        OutputError::Code(code) => {
          writeln!(f, "Backtick failed with exit code {}", code)?;
        },
        OutputError::Signal(signal) => {
          writeln!(f, "Backtick was terminated by signal {}", signal)?;
        },
        OutputError::Unknown => {
          writeln!(f, "Backtick failed for an unknown reason")?;
        },
        OutputError::Io(io_error) => {
          match io_error.kind() {
            io::ErrorKind::NotFound => write!(
              f,
              "Backtick could not be run because just could not find `sh`:\n{}",
              io_error
            ),
            io::ErrorKind::PermissionDenied => write!(
              f,
              "Backtick could not be run because just could not run `sh`:\n{}",
              io_error
            ),
            _ => write!(
              f,
              "Backtick could not be run because of an IO error while launching `sh`:\n{}",
              io_error
            ),
          }?;
        },
        OutputError::Utf8(utf8_error) => {
          writeln!(
            f,
            "Backtick succeeded but stdout was not utf8: {}",
            utf8_error
          )?;
        },
      },
      NoRecipes => {
        writeln!(f, "Justfile contains no recipes.",)?;
      },
      DefaultRecipeRequiresArguments {
        recipe,
        min_arguments,
      } => {
        writeln!(
          f,
          "Recipe `{}` cannot be used as default recipe since it requires at least {} {}.",
          recipe,
          min_arguments,
          Count("argument", *min_arguments),
        )?;
      },
      Internal { message } => {
        write!(
          f,
          "Internal runtime error, this may indicate a bug in just: {} \
           consider filing an issue: https://github.com/casey/just/issues/new",
          message
        )?;
      },
    }

    write!(f, "{}", message.suffix())?;

    if let Some(token) = self.context() {
      token.write_context(f, Color::fmt(f).error())?;
    }

    Ok(())
  }
}

impl<'src> From<dotenv::Error> for RuntimeError<'src> {
  fn from(dotenv_error: dotenv::Error) -> RuntimeError<'src> {
    RuntimeError::Dotenv { dotenv_error }
  }
}
