require 'json'
require 'ostruct'
require_relative '../layer/presenter'
require_relative '../layer_group/presenter'
require_relative '../named_map/presenter'

module CartoDB
  module Visualization
    class VizJSON
      include Carto::HtmlSafe

      VIZJSON_VERSION = '0.1.0'

      def initialize(visualization, options = {}, configuration = {}, logger = nil)
        @visualization    = visualization
        @map              = visualization.map
        @options          = default_options.merge(options)
        @configuration    = configuration
        @user             = options.fetch(:user, nil)
        logger.info(map.inspect) if logger
      end

      def to_export_poro(version = 1)
        description = if visualization.description.blank?
                        ""
                      else
                        clean_description(markdown_html_safe(visualization.description))
                      end

        {
          id:             visualization.id,
          version:        VIZJSON_VERSION,
          title:          visualization.qualified_name(@user),
          description:    description,
          scrollwheel:    map.scrollwheel,
          legends:        map.legends,
          url:            options.delete(:url),
          map_provider:   map.provider,
          bounds:         bounds_from(map),
          center:         map.center,
          zoom:           map.zoom,
          layers:         all_layers_for(visualization),
          overlays:       overlays_for(visualization),
          # Fields specific for this export
          export_version: version,
          # TODO: bug? @user is _viewer_user_, who might not be the owner
          owner:          { id: @user.id }
        }
      end

      # Return a PORO (Hash object) for easy JSONification
      # @see https://github.com/CartoDB/carto.js/blob/privacy-maps/doc/vizjson_format.md
      def to_poro
        poro_data = {
          id:             visualization.id,
          version:        VIZJSON_VERSION,
          title:          visualization.qualified_name(@user),
          description:    markdown_html_safe(visualization.description),
          scrollwheel:    map.scrollwheel,
          legends:        map.legends,
          url:            options.delete(:url),
          map_provider:   map.provider,
          bounds:         bounds_from(map),
          center:         map.center,
          zoom:           map.zoom,
          updated_at:     map.viz_updated_at,
          layers:         layers_for(visualization),
          overlays:       overlays_for(visualization),
          prev:           visualization.prev_id,
          next:           visualization.next_id,
          transition_options: visualization.transition_options
        }

        auth_tokens = auth_tokens_for(visualization)
        poro_data.merge!(auth_tokens: auth_tokens) if auth_tokens.length > 0

        unless visualization.parent_id.nil?
          poro_data[:title] = visualization.parent.qualified_name(@user)
          poro_data[:description] = markdown_html_safe(visualization.parent.description)
        end

        poro_data
      end

      def layer_group_for(visualization)
        LayerGroup::Presenter.new(visualization.layers(:cartodb), options, configuration).to_poro
      end

      def named_map_layer_group_for(visualization)
        LayerGroup::Presenter.new(visualization.layers(:named_map), options, configuration).to_poro
      end

      def other_layers_for(visualization, named_maps_presenter = nil)
        layer_index = visualization.layers(:cartodb).size

        visualization.layers(:others).map do |layer|
          if named_maps_presenter.nil?
            decoration_data_to_apply = {}
          else
            decoration_data_to_apply = named_maps_presenter.get_decoration_for_layer(layer.kind, layer_index)
          end
          layer_index += 1
          CartoDB::LayerModule::Presenter.new(layer, options, configuration, decoration_data_to_apply).to_vizjson_v2
        end
      end

      private

      attr_reader :visualization, :map, :options, :configuration

      # Redcarpet markdown renderer adds "garbage" that would otherwise get reimported
      def clean_description(description)
        description.sub(/^<p>/, "").sub(/<\/p> ?(\n)?$/, "")
      end

      def bounds_from(map)
        ::JSON.parse("[#{map.view_bounds_sw}, #{map.view_bounds_ne}]")
      rescue StandardError
        # Do nothing
      end

      def all_layers_for(visualization)
        layers_data = []

        basemap_layer = basemap_layer_for(visualization)
        layers_data.push(basemap_layer) if basemap_layer

        data_layers = visualization.layers(:cartodb).map do |layer|
          CartoDB::LayerModule::Presenter.new(layer, options, configuration).to_vizjson_v2
        end
        layers_data.push(data_layers)

        layers_data.push(other_layers_for(visualization))

        layers_data += non_basemap_base_layers_for(visualization)

        layers_data.compact.flatten
      end

      def layers_for(visualization)
        basemap_layer = basemap_layer_for(visualization)
        layers_data = []
        layers_data.push(basemap_layer) if basemap_layer

        if visualization.retrieve_named_map?
          presenter_options = {
            user_name: options.fetch(:user_name),
            api_key: options.delete(:user_api_key),
            https_request: options.fetch(:https_request, false),
            viewer_user: @user,
            owner: visualization.user
          }
          named_maps_presenter = CartoDB::NamedMapsWrapper::Presenter.new(
            visualization, layer_group_for_named_map(visualization), presenter_options, configuration
          )
          layers_data.push(named_maps_presenter.to_poro)
        else
          named_maps_presenter = nil
          layers_data.push(layer_group_for(visualization))
        end
        layers_data.push(other_layers_for(visualization, named_maps_presenter))

        layers_data += non_basemap_base_layers_for(visualization)

        layers_data.compact.flatten
      end

      def layer_group_for_named_map(visualization)
        layer_group_poro = layer_group_for(visualization)
        # If there is *only* a torque layer, there is no layergroup
        return {} if layer_group_poro.nil?

        layers_data = Array.new
        layer_num = 0
        layer_group_poro[:options][:layer_definition][:layers].each do |layer|
          layers_data.push(type:       layer[:type],
                           options:    layer[:options],
                           visible:    layer[:visible],
                           index:      layer_num
                          )
          layer_num += 1
        end
        layers_data
      end

      # INFO: Assumes layers come always ordered by order (they do)
      def basemap_layer_for(visualization)
        layer = visualization.layers(:base).first
        CartoDB::LayerModule::Presenter.new(layer, options, configuration).to_vizjson_v2 unless layer.nil?
      end

      # INFO: Assumes layers come always ordered by order (they do)
      def non_basemap_base_layers_for(visualization)
        base_layers = visualization.layers(:base)
        if base_layers.length > 0
          # Remove the basemap, which is always first
          base_layers.slice(1, visualization.layers(:base).length)
                     .map do |layer|
            CartoDB::LayerModule::Presenter.new(layer, options, configuration).to_vizjson_v2
          end
        else
          []
        end
      end

      def overlays_for(visualization)
        ordered_overlays_for(visualization).map do |overlay|
          Carto::Api::OverlayPresenter.new(overlay).to_vizjson_poro
        end
      end

      def ordered_overlays_for(visualization)
        visualization.overlays.to_a
      end

      def default_options
        {
          full: true,
          visualization_id: visualization.id,
          https_request: false,
          attributions: visualization.attributions_from_derived_visualizations,
          for_named_map: false
        }
      end

      def auth_tokens_for(visualization)
        visualization.has_password? ? visualization.get_auth_tokens : []
      end

    end
  end
end
