// sass.hpp must go before all system headers to get the
// __EXTENSIONS__ fix on Solaris.
#include "sass.hpp"

#include <iomanip>
#include "ast.hpp"
#include "fn_utils.hpp"
#include "fn_colors.hpp"
#include "util.hpp"
#include "util_string.hpp"

namespace Sass {

  namespace Functions {

    bool string_argument(AST_Node_Obj obj) {
      String_Constant* s = Cast<String_Constant>(obj);
      if (s == nullptr) return false;
      const sass::string& str = s->value();
      return starts_with(str, "calc(") ||
             starts_with(str, "var(");
    }

    void hsla_alpha_percent_deprecation(const SourceSpan& pstate, const sass::string val)
    {

      sass::string msg("Passing a percentage as the alpha value to hsla() will be interpreted");
      sass::string tail("differently in future versions of Sass. For now, use " + val + " instead.");

      deprecated(msg, tail, false, pstate);

    }

    Signature rgb_sig = "rgb($red, $green, $blue)";
    BUILT_IN(rgb)
    {
      if (
        string_argument(env["$red"]) ||
        string_argument(env["$green"]) ||
        string_argument(env["$blue"])
      ) {
        return SASS_MEMORY_NEW(String_Constant, pstate, "rgb("
                                                        + env["$red"]->to_string()
                                                        + ", "
                                                        + env["$green"]->to_string()
                                                        + ", "
                                                        + env["$blue"]->to_string()
                                                        + ")"
        );
      }

      return SASS_MEMORY_NEW(Color_RGBA,
                             pstate,
                             COLOR_NUM("$red"),
                             COLOR_NUM("$green"),
                             COLOR_NUM("$blue"));
    }

    Signature rgba_4_sig = "rgba($red, $green, $blue, $alpha)";
    BUILT_IN(rgba_4)
    {
      if (
        string_argument(env["$red"]) ||
        string_argument(env["$green"]) ||
        string_argument(env["$blue"]) ||
        string_argument(env["$alpha"])
      ) {
        return SASS_MEMORY_NEW(String_Constant, pstate, "rgba("
                                                        + env["$red"]->to_string()
                                                        + ", "
                                                        + env["$green"]->to_string()
                                                        + ", "
                                                        + env["$blue"]->to_string()
                                                        + ", "
                                                        + env["$alpha"]->to_string()
                                                        + ")"
        );
      }

      return SASS_MEMORY_NEW(Color_RGBA,
                             pstate,
                             COLOR_NUM("$red"),
                             COLOR_NUM("$green"),
                             COLOR_NUM("$blue"),
                             ALPHA_NUM("$alpha"));
    }

    Signature rgba_2_sig = "rgba($color, $alpha)";
    BUILT_IN(rgba_2)
    {
      if (
        string_argument(env["$color"])
      ) {
        return SASS_MEMORY_NEW(String_Constant, pstate, "rgba("
                                                        + env["$color"]->to_string()
                                                        + ", "
                                                        + env["$alpha"]->to_string()
                                                        + ")"
        );
      }

      Color_RGBA_Obj c_arg = ARG("$color", Color)->toRGBA();

      if (
        string_argument(env["$alpha"])
      ) {
        sass::ostream strm;
        strm << "rgba("
                 << (int)c_arg->r() << ", "
                 << (int)c_arg->g() << ", "
                 << (int)c_arg->b() << ", "
                 << env["$alpha"]->to_string()
             << ")";
        return SASS_MEMORY_NEW(String_Constant, pstate, strm.str());
      }

      Color_RGBA_Obj new_c = SASS_MEMORY_COPY(c_arg);
      new_c->a(ALPHA_NUM("$alpha"));
      new_c->disp("");
      return new_c.detach();
    }

    ////////////////
    // RGB FUNCTIONS
    ////////////////

    Signature red_sig = "red($color)";
    BUILT_IN(red)
    {
      Color_RGBA_Obj color = ARG("$color", Color)->toRGBA();
      return SASS_MEMORY_NEW(Number, pstate, color->r());
    }

    Signature green_sig = "green($color)";
    BUILT_IN(green)
    {
      Color_RGBA_Obj color = ARG("$color", Color)->toRGBA();
      return SASS_MEMORY_NEW(Number, pstate, color->g());
    }

    Signature blue_sig = "blue($color)";
    BUILT_IN(blue)
    {
      Color_RGBA_Obj color = ARG("$color", Color)->toRGBA();
      return SASS_MEMORY_NEW(Number, pstate, color->b());
    }

    Color_RGBA* colormix(Context& ctx, SourceSpan& pstate, Color* color1, Color* color2, double weight) {
      Color_RGBA_Obj c1 = color1->toRGBA();
      Color_RGBA_Obj c2 = color2->toRGBA();
      double p = weight/100;
      double w = 2*p - 1;
      double a = c1->a() - c2->a();

      double w1 = (((w * a == -1) ? w : (w + a)/(1 + w*a)) + 1)/2.0;
      double w2 = 1 - w1;

      return SASS_MEMORY_NEW(Color_RGBA,
                             pstate,
                             Sass::round(w1*c1->r() + w2*c2->r(), ctx.c_options.precision),
                             Sass::round(w1*c1->g() + w2*c2->g(), ctx.c_options.precision),
                             Sass::round(w1*c1->b() + w2*c2->b(), ctx.c_options.precision),
                             c1->a()*p + c2->a()*(1-p));
    }

    Signature mix_sig = "mix($color1, $color2, $weight: 50%)";
    BUILT_IN(mix)
    {
      Color_Obj  color1 = ARG("$color1", Color);
      Color_Obj  color2 = ARG("$color2", Color);
      double weight = DARG_U_PRCT("$weight");
      return colormix(ctx, pstate, color1, color2, weight);

    }

    ////////////////
    // HSL FUNCTIONS
    ////////////////

    Signature hsl_sig = "hsl($hue, $saturation, $lightness)";
    BUILT_IN(hsl)
    {
      if (
        string_argument(env["$hue"]) ||
        string_argument(env["$saturation"]) ||
        string_argument(env["$lightness"])
      ) {
        return SASS_MEMORY_NEW(String_Constant, pstate, "hsl("
                                                        + env["$hue"]->to_string()
                                                        + ", "
                                                        + env["$saturation"]->to_string()
                                                        + ", "
                                                        + env["$lightness"]->to_string()
                                                        + ")"
        );
      }

      return SASS_MEMORY_NEW(Color_HSLA,
        pstate,
        ARGVAL("$hue"),
        ARGVAL("$saturation"),
        ARGVAL("$lightness"),
        1.0);

    }

    Signature hsla_sig = "hsla($hue, $saturation, $lightness, $alpha)";
    BUILT_IN(hsla)
    {
      if (
        string_argument(env["$hue"]) ||
        string_argument(env["$saturation"]) ||
        string_argument(env["$lightness"]) ||
        string_argument(env["$alpha"])
      ) {
        return SASS_MEMORY_NEW(String_Constant, pstate, "hsla("
                                                        + env["$hue"]->to_string()
                                                        + ", "
                                                        + env["$saturation"]->to_string()
                                                        + ", "
                                                        + env["$lightness"]->to_string()
                                                        + ", "
                                                        + env["$alpha"]->to_string()
                                                        + ")"
        );
      }

      Number* alpha = ARG("$alpha", Number);
      if (alpha && alpha->unit() == "%") {
        Number_Obj val = SASS_MEMORY_COPY(alpha);
        val->numerators.clear(); // convert
        val->value(val->value() / 100.0);
        sass::string nr(val->to_string(ctx.c_options));
        hsla_alpha_percent_deprecation(pstate, nr);
      }

      return SASS_MEMORY_NEW(Color_HSLA,
        pstate,
        ARGVAL("$hue"),
        ARGVAL("$saturation"),
        ARGVAL("$lightness"),
        ARGVAL("$alpha"));

    }

    /////////////////////////////////////////////////////////////////////////
    // Query functions
    /////////////////////////////////////////////////////////////////////////

    Signature hue_sig = "hue($color)";
    BUILT_IN(hue)
    {
      Color_HSLA_Obj col = ARG("$color", Color)->toHSLA();
      return SASS_MEMORY_NEW(Number, pstate, col->h(), "deg");
    }

    Signature saturation_sig = "saturation($color)";
    BUILT_IN(saturation)
    {
      Color_HSLA_Obj col = ARG("$color", Color)->toHSLA();
      return SASS_MEMORY_NEW(Number, pstate, col->s(), "%");
    }

    Signature lightness_sig = "lightness($color)";
    BUILT_IN(lightness)
    {
      Color_HSLA_Obj col = ARG("$color", Color)->toHSLA();
      return SASS_MEMORY_NEW(Number, pstate, col->l(), "%");
    }

    /////////////////////////////////////////////////////////////////////////
    // HSL manipulation functions
    /////////////////////////////////////////////////////////////////////////

    Signature adjust_hue_sig = "adjust-hue($color, $degrees)";
    BUILT_IN(adjust_hue)
    {
      Color* col = ARG("$color", Color);
      double degrees = ARGVAL("$degrees");
      Color_HSLA_Obj copy = col->copyAsHSLA();
      copy->h(absmod(copy->h() + degrees, 360.0));
      return copy.detach();
    }

    Signature lighten_sig = "lighten($color, $amount)";
    BUILT_IN(lighten)
    {
      Color* col = ARG("$color", Color);
      double amount = DARG_U_PRCT("$amount");
      Color_HSLA_Obj copy = col->copyAsHSLA();
      copy->l(clip(copy->l() + amount, 0.0, 100.0));
      return copy.detach();

    }

    Signature darken_sig = "darken($color, $amount)";
    BUILT_IN(darken)
    {
      Color* col = ARG("$color", Color);
      double amount = DARG_U_PRCT("$amount");
      Color_HSLA_Obj copy = col->copyAsHSLA();
      copy->l(clip(copy->l() - amount, 0.0, 100.0));
      return copy.detach();
    }

    Signature saturate_sig = "saturate($color, $amount: false)";
    BUILT_IN(saturate)
    {
      // CSS3 filter function overload: pass literal through directly
      if (!Cast<Number>(env["$amount"])) {
        return SASS_MEMORY_NEW(String_Quoted, pstate, "saturate(" + env["$color"]->to_string(ctx.c_options) + ")");
      }

      Color* col = ARG("$color", Color);
      double amount = DARG_U_PRCT("$amount");
      Color_HSLA_Obj copy = col->copyAsHSLA();
      copy->s(clip(copy->s() + amount, 0.0, 100.0));
      return copy.detach();
    }

    Signature desaturate_sig = "desaturate($color, $amount)";
    BUILT_IN(desaturate)
    {
      Color* col = ARG("$color", Color);
      double amount = DARG_U_PRCT("$amount");
      Color_HSLA_Obj copy = col->copyAsHSLA();
      copy->s(clip(copy->s() - amount, 0.0, 100.0));
      return copy.detach();
    }

    Signature grayscale_sig = "grayscale($color)";
    BUILT_IN(grayscale)
    {
      // CSS3 filter function overload: pass literal through directly
      Number* amount = Cast<Number>(env["$color"]);
      if (amount) {
        return SASS_MEMORY_NEW(String_Quoted, pstate, "grayscale(" + amount->to_string(ctx.c_options) + ")");
      }

      Color* col = ARG("$color", Color);
      Color_HSLA_Obj copy = col->copyAsHSLA();
      copy->s(0.0); // just reset saturation
      return copy.detach();
    }

    /////////////////////////////////////////////////////////////////////////
    // Misc manipulation functions
    /////////////////////////////////////////////////////////////////////////

    Signature complement_sig = "complement($color)";
    BUILT_IN(complement)
    {
      Color* col = ARG("$color", Color);
      Color_HSLA_Obj copy = col->copyAsHSLA();
      copy->h(absmod(copy->h() - 180.0, 360.0));
      return copy.detach();
    }

    Signature invert_sig = "invert($color, $weight: 100%)";
    BUILT_IN(invert)
    {
      // CSS3 filter function overload: pass literal through directly
      Number* amount = Cast<Number>(env["$color"]);
      double weight = DARG_U_PRCT("$weight");
      if (amount) {
        // TODO: does not throw on 100% manually passed as value
        if (weight < 100.0) {
          error("Only one argument may be passed to the plain-CSS invert() function.", pstate, traces);
        }
        return SASS_MEMORY_NEW(String_Quoted, pstate, "invert(" + amount->to_string(ctx.c_options) + ")");
      }

      Color* col = ARG("$color", Color);
      Color_RGBA_Obj inv = col->copyAsRGBA();
      inv->r(clip(255.0 - inv->r(), 0.0, 255.0));
      inv->g(clip(255.0 - inv->g(), 0.0, 255.0));
      inv->b(clip(255.0 - inv->b(), 0.0, 255.0));
      return colormix(ctx, pstate, inv, col, weight);
    }

    /////////////////////////////////////////////////////////////////////////
    // Opacity functions
    /////////////////////////////////////////////////////////////////////////

    Signature alpha_sig = "alpha($color)";
    Signature opacity_sig = "opacity($color)";
    BUILT_IN(alpha)
    {
      String_Constant* ie_kwd = Cast<String_Constant>(env["$color"]);
      if (ie_kwd) {
        return SASS_MEMORY_NEW(String_Quoted, pstate, "alpha(" + ie_kwd->value() + ")");
      }

      // CSS3 filter function overload: pass literal through directly
      Number* amount = Cast<Number>(env["$color"]);
      if (amount) {
        return SASS_MEMORY_NEW(String_Quoted, pstate, "opacity(" + amount->to_string(ctx.c_options) + ")");
      }

      return SASS_MEMORY_NEW(Number, pstate, ARG("$color", Color)->a());
    }

    Signature opacify_sig = "opacify($color, $amount)";
    Signature fade_in_sig = "fade-in($color, $amount)";
    BUILT_IN(opacify)
    {
      Color* col = ARG("$color", Color);
      double amount = DARG_U_FACT("$amount");
      Color_Obj copy = SASS_MEMORY_COPY(col);
      copy->a(clip(col->a() + amount, 0.0, 1.0));
      return copy.detach();
    }

    Signature transparentize_sig = "transparentize($color, $amount)";
    Signature fade_out_sig = "fade-out($color, $amount)";
    BUILT_IN(transparentize)
    {
      Color* col = ARG("$color", Color);
      double amount = DARG_U_FACT("$amount");
      Color_Obj copy = SASS_MEMORY_COPY(col);
      copy->a(std::max(col->a() - amount, 0.0));
      return copy.detach();
    }

    ////////////////////////
    // OTHER COLOR FUNCTIONS
    ////////////////////////

    Signature adjust_color_sig = "adjust-color($color, $red: false, $green: false, $blue: false, $hue: false, $saturation: false, $lightness: false, $alpha: false)";
    BUILT_IN(adjust_color)
    {
      Color* col = ARG("$color", Color);
      Number* r = Cast<Number>(env["$red"]);
      Number* g = Cast<Number>(env["$green"]);
      Number* b = Cast<Number>(env["$blue"]);
      Number* h = Cast<Number>(env["$hue"]);
      Number* s = Cast<Number>(env["$saturation"]);
      Number* l = Cast<Number>(env["$lightness"]);
      Number* a = Cast<Number>(env["$alpha"]);

      bool rgb = r || g || b;
      bool hsl = h || s || l;

      if (rgb && hsl) {
        error("Cannot specify HSL and RGB values for a color at the same time for `adjust-color'", pstate, traces);
      }
      else if (rgb) {
        Color_RGBA_Obj c = col->copyAsRGBA();
        if (r) c->r(c->r() + DARG_R_BYTE("$red"));
        if (g) c->g(c->g() + DARG_R_BYTE("$green"));
        if (b) c->b(c->b() + DARG_R_BYTE("$blue"));
        if (a) c->a(c->a() + DARG_R_FACT("$alpha"));
        return c.detach();
      }
      else if (hsl) {
        Color_HSLA_Obj c = col->copyAsHSLA();
        if (h) c->h(c->h() + absmod(h->value(), 360.0));
        if (s) c->s(c->s() + DARG_R_PRCT("$saturation"));
        if (l) c->l(c->l() + DARG_R_PRCT("$lightness"));
        if (a) c->a(c->a() + DARG_R_FACT("$alpha"));
        return c.detach();
      }
      else if (a) {
        Color_Obj c = SASS_MEMORY_COPY(col);
        c->a(c->a() + DARG_R_FACT("$alpha"));
        c->a(clip(c->a(), 0.0, 1.0));
        return c.detach();
      }
      error("not enough arguments for `adjust-color'", pstate, traces);
      // unreachable
      return col;
    }

    Signature scale_color_sig = "scale-color($color, $red: false, $green: false, $blue: false, $hue: false, $saturation: false, $lightness: false, $alpha: false)";
    BUILT_IN(scale_color)
    {
      Color* col = ARG("$color", Color);
      Number* r = Cast<Number>(env["$red"]);
      Number* g = Cast<Number>(env["$green"]);
      Number* b = Cast<Number>(env["$blue"]);
      Number* h = Cast<Number>(env["$hue"]);
      Number* s = Cast<Number>(env["$saturation"]);
      Number* l = Cast<Number>(env["$lightness"]);
      Number* a = Cast<Number>(env["$alpha"]);

      bool rgb = r || g || b;
      bool hsl = h || s || l;

      if (rgb && hsl) {
        error("Cannot specify HSL and RGB values for a color at the same time for `scale-color'", pstate, traces);
      }
      else if (rgb) {
        Color_RGBA_Obj c = col->copyAsRGBA();
        double rscale = (r ? DARG_R_PRCT("$red") : 0.0) / 100.0;
        double gscale = (g ? DARG_R_PRCT("$green") : 0.0) / 100.0;
        double bscale = (b ? DARG_R_PRCT("$blue") : 0.0) / 100.0;
        double ascale = (a ? DARG_R_PRCT("$alpha") : 0.0) / 100.0;
        if (rscale) c->r(c->r() + rscale * (rscale > 0.0 ? 255.0 - c->r() : c->r()));
        if (gscale) c->g(c->g() + gscale * (gscale > 0.0 ? 255.0 - c->g() : c->g()));
        if (bscale) c->b(c->b() + bscale * (bscale > 0.0 ? 255.0 - c->b() : c->b()));
        if (ascale) c->a(c->a() + ascale * (ascale > 0.0 ? 1.0 - c->a() : c->a()));
        return c.detach();
      }
      else if (hsl) {
        Color_HSLA_Obj c = col->copyAsHSLA();
        double hscale = (h ? DARG_R_PRCT("$hue") : 0.0) / 100.0;
        double sscale = (s ? DARG_R_PRCT("$saturation") : 0.0) / 100.0;
        double lscale = (l ? DARG_R_PRCT("$lightness") : 0.0) / 100.0;
        double ascale = (a ? DARG_R_PRCT("$alpha") : 0.0) / 100.0;
        if (hscale) c->h(c->h() + hscale * (hscale > 0.0 ? 360.0 - c->h() : c->h()));
        if (sscale) c->s(c->s() + sscale * (sscale > 0.0 ? 100.0 - c->s() : c->s()));
        if (lscale) c->l(c->l() + lscale * (lscale > 0.0 ? 100.0 - c->l() : c->l()));
        if (ascale) c->a(c->a() + ascale * (ascale > 0.0 ? 1.0 - c->a() : c->a()));
        return c.detach();
      }
      else if (a) {
        Color_Obj c = SASS_MEMORY_COPY(col);
        double ascale = DARG_R_PRCT("$alpha") / 100.0;
        c->a(c->a() + ascale * (ascale > 0.0 ? 1.0 - c->a() : c->a()));
        c->a(clip(c->a(), 0.0, 1.0));
        return c.detach();
      }
      error("not enough arguments for `scale-color'", pstate, traces);
      // unreachable
      return col;
    }

    Signature change_color_sig = "change-color($color, $red: false, $green: false, $blue: false, $hue: false, $saturation: false, $lightness: false, $alpha: false)";
    BUILT_IN(change_color)
    {
      Color* col = ARG("$color", Color);
      Number* r = Cast<Number>(env["$red"]);
      Number* g = Cast<Number>(env["$green"]);
      Number* b = Cast<Number>(env["$blue"]);
      Number* h = Cast<Number>(env["$hue"]);
      Number* s = Cast<Number>(env["$saturation"]);
      Number* l = Cast<Number>(env["$lightness"]);
      Number* a = Cast<Number>(env["$alpha"]);

      bool rgb = r || g || b;
      bool hsl = h || s || l;

      if (rgb && hsl) {
        error("Cannot specify HSL and RGB values for a color at the same time for `change-color'", pstate, traces);
      }
      else if (rgb) {
        Color_RGBA_Obj c = col->copyAsRGBA();
        if (r) c->r(DARG_U_BYTE("$red"));
        if (g) c->g(DARG_U_BYTE("$green"));
        if (b) c->b(DARG_U_BYTE("$blue"));
        if (a) c->a(DARG_U_FACT("$alpha"));
        return c.detach();
      }
      else if (hsl) {
        Color_HSLA_Obj c = col->copyAsHSLA();
        if (h) c->h(absmod(h->value(), 360.0));
        if (s) c->s(DARG_U_PRCT("$saturation"));
        if (l) c->l(DARG_U_PRCT("$lightness"));
        if (a) c->a(DARG_U_FACT("$alpha"));
        return c.detach();
      }
      else if (a) {
        Color_Obj c = SASS_MEMORY_COPY(col);
        c->a(clip(DARG_U_FACT("$alpha"), 0.0, 1.0));
        return c.detach();
      }
      error("not enough arguments for `change-color'", pstate, traces);
      // unreachable
      return col;
    }

    Signature ie_hex_str_sig = "ie-hex-str($color)";
    BUILT_IN(ie_hex_str)
    {
      Color* col = ARG("$color", Color);
      Color_RGBA_Obj c = col->toRGBA();
      double r = clip(c->r(), 0.0, 255.0);
      double g = clip(c->g(), 0.0, 255.0);
      double b = clip(c->b(), 0.0, 255.0);
      double a = clip(c->a(), 0.0, 1.0) * 255.0;

      sass::ostream ss;
      ss << '#' << std::setw(2) << std::setfill('0');
      ss << std::hex << std::setw(2) << static_cast<unsigned long>(Sass::round(a, ctx.c_options.precision));
      ss << std::hex << std::setw(2) << static_cast<unsigned long>(Sass::round(r, ctx.c_options.precision));
      ss << std::hex << std::setw(2) << static_cast<unsigned long>(Sass::round(g, ctx.c_options.precision));
      ss << std::hex << std::setw(2) << static_cast<unsigned long>(Sass::round(b, ctx.c_options.precision));

      sass::string result = ss.str();
      Util::ascii_str_toupper(&result);
      return SASS_MEMORY_NEW(String_Quoted, pstate, result);
    }

  }

}