module Colors
  class LinearSegmentedColormap < Colormap
    def initialize(name, segmented_data, n_colors: 256, gamma: 1.0)
      super(name, n_colors)

      @monochrome = false
      @segmented_data = segmented_data
      @gamma = gamma
    end

    attr_reader :segmented_data, :gamma

    def self.new_from_list(name, colors, n_colors: 256, gamma: 1.0)
      case colors
      when Enumerable
        colors = colors.to_a
      else
        ary = Array.try_convert(colors)
        if ary.nil?
          raise ArgumentError, "colors must be convertible to an array: %p for %s" % [colors, name]
        else
          colors = ary
        end
      end

      case colors[0]
      when Array
        unless colors.all? {|a| a.length == 2 }
          raise ArgumentError, "colors array has invalid items" 
        end
        vals, colors = colors.transpose
      else
        vals = Utils.linspace(0r, 1r, colors.length)
      end

      r, g, b, a = colors.map { |c|
        Utils.make_color(c).to_rgba.components
      }.transpose

      segmented_data = {
        red:   [vals, r, r].transpose,
        green: [vals, g, g].transpose,
        blue:  [vals, b, b].transpose,
        alpha: [vals, a, a].transpose
      }

      new(name, segmented_data, n_colors: n_colors, gamma: gamma)
    end

    def gamma=(val)
      @gamma = val
      @initialized = false
    end

    private def init_colormap
      red = create_lookup_table(self.n_colors, @segmented_data[:red], @gamma)
      green = create_lookup_table(self.n_colors, @segmented_data[:green], @gamma)
      blue = create_lookup_table(self.n_colors, @segmented_data[:blue], @gamma)
      alpha = if @segmented_data.key?(:alpha)
                create_lookup_table(self.n_colors, @segmented_data[:alpha], @gamma)
              end
      @lookup_table = Array.new(self.n_colors) do |i|
        Colors::RGBA.new(red[i], green[i], blue[i], alpha ? alpha[i] : 1r)
      end
      @initialized = true
      update_extreme_colors
    end

    private def create_lookup_table(n, data, gamma=1.0)
      if data.respond_to?(:call)
        xind = Utils.linspace(0r, 1r, n).map {|x| x ** gamma }
        lut = xind.map {|i| data.(i).clamp(0, 1).to_f }
        return lut
      end

      ary = Array.try_convert(data)
      if ary.nil?
        raise ArgumentError, "data must be convertible to an array"
      elsif ary.any? {|sub| sub.length != 3 }
        raise ArgumentError, "data array must consist of 3-length arrays"
      end

      x, y0, y1 = ary.transpose

      if x[0] != 0.0 || x[-1] != 1.0
        raise ArgumentError,
              "data mapping points must start with x=0 and end with x=1"
      end

      unless x.each_cons(2).all? {|a, b| a < b }
        raise ArgumentError,
              "data mapping points must have x in increasing order"
      end

      if n == 1
        # Use the `y = f(x=1)` value for a 1-element lookup table
        lut = [y0[-1]]
      else
        x.map! {|v| v.to_f * (n - 1) }
        xind = Utils.linspace(0r, 1r, n).map {|i| (n - 1) * i ** gamma }
        ind = (0 ... n).map {|i| x.find_index {|v| xind[i] < v } }[1 ... -1]

        distance = ind.map.with_index do |i, j|
          (xind[j+1] - x[i - 1]) / (x[i] - x[i - 1])
        end

        lut = [
          y1[0],
          *ind.map.with_index {|i, j| distance[j] * (y0[i] - y1[i - 1]) + y1[i - 1] },
          y0[-1]
        ]
      end

      return lut.map {|v| v.clamp(0, 1).to_f }
    end

    private def make_reverse_colormap(name)
      segmented_data_r = self.segmented_data.map { |key, data|
        data_r = if data.respond_to?(:call)
                   make_inverse_func(data)
                 else
                   data.reverse_each.map do |x, y0, y1|
                     [1r - x, y1, y0]
                   end
                 end
        [key, data_r]
      }.to_h
      LinearSegmentedColormap.new(name, segmented_data_r, n_colors: self.n_colors, gamma: self.gamma)
    end

    private def make_inverse_func(f)
      ->(x) { f(1 - x) }
    end
  end
end
