require_relative '../models/visualization/collection'
require_relative '../models/table/user_table'
require_dependency 'carto/bounding_box_utils'
require_dependency 'common/map_common'

class Map < Sequel::Model
  include Carto::MapBoundaries

  self.raise_on_save_failure = false

  plugin :serialization, :json, :options

  one_to_many :tables, class: ::UserTable
  many_to_one :user

  many_to_many :layers,
               order: :order,
               after_add: proc { |map, layer| layer.after_added_to_map(map) }

  many_to_many :base_layers,
               clone: :layers,
               right_key: :layer_id

  many_to_many :carto_layers,
               clone: :layers,
               right_key: :layer_id,
               conditions: { kind: "carto" }

  many_to_many :data_layers,
               clone: :layers,
               right_key: :layer_id,
               conditions: "kind in ('carto', 'torque')"

  many_to_many :user_layers,
               clone: :layers,
               right_key: :layer_id,
               conditions: "kind in ('tiled', 'background', 'gmapsbase', 'wms')"

  many_to_many :torque_layers,
               clone: :layers,
               right_key: :layer_id,
               conditions: { kind: "torque" }

  many_to_many :other_layers,
               clone: :layers,
               right_key: :layer_id,
               conditions: "kind not in ('carto', 'tiled', 'background', 'gmapsbase', 'wms')"

  many_to_many :named_maps_layers,
               clone: :layers,
               right_key: :layer_id,
               conditions: "kind in ('tiled', 'background', 'gmapsbase', 'wms', 'carto')"

  plugin :association_dependencies, layers: :nullify

  PUBLIC_ATTRIBUTES = %w{ id user_id provider bounding_box_sw
                          bounding_box_ne center zoom view_bounds_sw view_bounds_ne legends scrollwheel }

  # FE code, so (lat,lon)
  DEFAULT_OPTIONS = {
    zoom:            3,
    bounding_box_sw: [Carto::BoundingBoxUtils::DEFAULT_BOUNDS[:miny],
                      Carto::BoundingBoxUtils::DEFAULT_BOUNDS[:minx]],
    bounding_box_ne: [Carto::BoundingBoxUtils::DEFAULT_BOUNDS[:maxy],
                      Carto::BoundingBoxUtils::DEFAULT_BOUNDS[:maxx]],
    provider:        'leaflet',
    center:          [30, 0]
  }

  attr_accessor :table_id,
                # Flag to detect if being destroyed by whom so invalidate_vizjson_varnish_cache skips it
                :being_destroyed_by_vis_id

  def before_save
    super
    self.updated_at = Time.now
  end

  def after_save
    super
    update_map_on_associated_entities
    notify_map_change
  end

  def notify_map_change
    visualization = visualizations.first

    force_notify_map_change
  end

  def force_notify_map_change
    update_related_named_maps
    invalidate_vizjson_varnish_cache
  end

  def before_destroy
    raise CartoDB::InvalidMember.new(user: "Viewer users can't destroy maps") if user && user.viewer
    layers.each(&:destroy)
    super
    invalidate_vizjson_varnish_cache
  end

  def public_values
    Hash[PUBLIC_ATTRIBUTES.map { |a| [a, send(a)] }]
  end

  def validate
    super
    errors.add(:user_id, "can't be blank") if user_id.blank?
    errors.add(:user, "Viewer users can't save maps") if user && user.viewer
  end

  def viz_updated_at
    latest_mapcap = visualizations.first.latest_mapcap

    latest_mapcap ? latest_mapcap.created_at : get_the_last_time_tiles_have_changed_to_render_it_in_vizjsons
  end

  def invalidate_vizjson_varnish_cache
    visualizations.each do |visualization|
      visualization.invalidate_cache unless visualization.id == being_destroyed_by_vis_id
    end
  end

  def update_related_named_maps
    visualizations.each do |visualization|
      visualization.save_named_map unless visualization.id == being_destroyed_by_vis_id
    end
  end

  def admits_layer?(layer)
    return admits_more_torque_layers? if layer.torque_layer?
    return admits_more_data_layers? if layer.data_layer?
    return admits_more_base_layers?(layer) if layer.base_layer?
  end

  def can_add_layer?(user, layer)
    return true if layer.base_layer?
    return false if user.max_layers && user.max_layers <= data_layers.count
    return false if user.viewer

    current_vis = visualizations.first
    current_vis.has_permission?(user, CartoDB::Visualization::Member::PERMISSION_READWRITE)
  end

  def visualizations
    @visualizations_collection ||= CartoDB::Visualization::Collection.new.fetch(map_id: [id]).to_a
  end

  def visualization
    visualizations.first
  end

  def process_privacy_in(layer)
    return self unless layer.uses_private_tables?

    visualizations.each do |visualization|
      if visualization.can_be_private?
        visualization.privacy = CartoDB::Visualization::Member::PRIVACY_PRIVATE
        visualization.store
      end
    end
  end

  def self.provider_for_baselayer_kind(kind)
    kind == 'tiled' ? 'leaflet' : 'googlemaps'
  end

  # (lat,lon) points on all map data
  def center_data
    center.presence || DEFAULT_OPTIONS[:center]
  end

  def view_bounds_data
    bbox_sw = view_bounds_sw.presence || DEFAULT_OPTIONS[:bounding_box_sw]
    bbox_ne = view_bounds_ne.presence || DEFAULT_OPTIONS[:bounding_box_ne]

    {
      # LowerCorner longitude, in decimal degrees
      west:  bbox_sw[1],
      # LowerCorner latitude, in decimal degrees
      south: bbox_sw[0],
      # UpperCorner longitude, in decimal degrees
      east:  bbox_ne[1],
      # UpperCorner latitude, in decimal degrees
      north: bbox_ne[0]
    }
  end

  def dup
    map = Map.new(to_hash.reject { |k, _| [:id, :options].include?(k) })
    map.options = options # Manually copied to avoid serialization
    map
  end

  private

  def get_the_last_time_tiles_have_changed_to_render_it_in_vizjsons
    table       = tables.first
    from_table  = table.service.data_last_modified if table

    [from_table, data_layers.map(&:updated_at)].flatten.compact.max
  end

  def update_map_on_associated_entities
    return unless table_id

    # Cannot filter by user_id as might be a shared table not owned by us
    related_table = ::UserTable.filter(id: table_id).first
    if related_table.map_id != id
      # Manually propagate to visualization (@see Table.after_save) if exists (at table creation won't)
      if related_table.map_id.present?
        CartoDB::Visualization::Collection.new.fetch(
          user_id: user_id,
          map_id: related_table.map_id
        ).each { |entry| entry.store_from_map(map_id: id) }
      end
      # HERE BE DRAGONS! If we try to store using model, callbacks break hell. Manual update required
      related_table.this.update(map_id: id)
    end
  end

  def table_visualization?
    !!table_visualization
  end

  def table_visualization
    CartoDB::Visualization::Collection.new
                                      .fetch(map_id: [id], type: CartoDB::Visualization::Member::TYPE_CANONICAL)
                                      .first
  end

  def admits_more_data_layers?
    data_layers.length >= 1 && table_visualization? ? false : true
  end

  def admits_more_torque_layers?
    torque_layers.length >= 1 && table_visualization? ? false : true
  end

  def admits_more_base_layers?(layer)
    # no basemap layer, always allow
    return true if user_layers.length < 1
    # have basemap? then allow only if comes on top (for labels)
    layer.order >= layers.last.order && user_layers.length >= 1
  end

  def table_name
    tables.first.nil? ? nil : tables.first.name
  end
end
