# frozen_string_literal: true

#
# Copyright (C) 2015 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.

require 'pathname'
require 'yaml'
require 'open3'

module BrandableCSS
  APP_ROOT = defined?(Rails) && Rails.root || Pathname.pwd
  CONFIG = YAML.load_file(APP_ROOT.join('config/brandable_css.yml')).freeze
  BRANDABLE_VARIABLES = JSON.parse(File.read(APP_ROOT.join(CONFIG['paths']['brandable_variables_json']))).freeze
  MIGRATION_NAME = 'RegenerateBrandFilesBasedOnNewDefaults'.freeze

  use_compressed = (defined?(Rails) && Rails.env.production?) || (ENV['RAILS_ENV'] == 'production')
  SASS_STYLE = ENV['SASS_STYLE'] || ((use_compressed ? 'compressed' : 'nested')).freeze

  VARIABLE_HUMAN_NAMES = {
    "ic-brand-primary" => lambda { I18n.t("Primary Brand Color") },
    "ic-brand-font-color-dark" => lambda { I18n.t("Main Text Color") },
    "ic-link-color" => lambda { I18n.t("Link Color") },
    "ic-brand-button--primary-bgd" => lambda { I18n.t("Primary Button") },
    "ic-brand-button--primary-text" => lambda { I18n.t("Primary Button Text") },
    "ic-brand-button--secondary-bgd" => lambda { I18n.t("Secondary Button") },
    "ic-brand-button--secondary-text" => lambda { I18n.t("Secondary Button Text") },
    "ic-brand-global-nav-bgd" => lambda { I18n.t("Nav Background") },
    "ic-brand-global-nav-ic-icon-svg-fill" => lambda { I18n.t("Nav Icon") },
    "ic-brand-global-nav-ic-icon-svg-fill--active" => lambda { I18n.t("Nav Icon Active") },
    "ic-brand-global-nav-menu-item__text-color" => lambda { I18n.t("Nav Text") },
    "ic-brand-global-nav-menu-item__text-color--active" => lambda { I18n.t("Nav Text Active") },
    "ic-brand-global-nav-avatar-border" => lambda { I18n.t("Nav Avatar Border") },
    "ic-brand-global-nav-menu-item__badge-bgd" => lambda { I18n.t("Nav Badge") },
    "ic-brand-global-nav-menu-item__badge-text" => lambda { I18n.t("Nav Badge Text") },
    "ic-brand-global-nav-logo-bgd" => lambda { I18n.t("Nav Logo Background") },
    "ic-brand-header-image" => lambda { I18n.t("Nav Logo") },
    "ic-brand-mobile-global-nav-logo" => lambda { I18n.t("Responsive Global Nav Logo") },
    "ic-brand-watermark" => lambda { I18n.t("Watermark") },
    "ic-brand-watermark-opacity" => lambda { I18n.t("Watermark Opacity") },
    "ic-brand-favicon" => lambda { I18n.t("Favicon") },
    "ic-brand-apple-touch-icon" => lambda { I18n.t("Mobile Homescreen Icon") },
    "ic-brand-msapplication-tile-color" => lambda { I18n.t("Windows Tile Color") },
    "ic-brand-msapplication-tile-square" => lambda { I18n.t("Windows Tile: Square") },
    "ic-brand-msapplication-tile-wide" => lambda { I18n.t("Windows Tile: Wide") },
    "ic-brand-right-sidebar-logo" => lambda { I18n.t("Right Sidebar Logo") },
    "ic-brand-Login-body-bgd-color" => lambda { I18n.t("Background Color") },
    "ic-brand-Login-body-bgd-image" => lambda { I18n.t("Background Image") },
    "ic-brand-Login-body-bgd-shadow-color" => lambda { I18n.t("Body Shadow") },
    "ic-brand-Login-logo" => lambda { I18n.t("Login Logo") },
    "ic-brand-Login-Content-bgd-color" => lambda { I18n.t("Top Box Background") },
    "ic-brand-Login-Content-border-color" => lambda { I18n.t("Top Box Border") },
    "ic-brand-Login-Content-inner-bgd" => lambda { I18n.t("Inner Box Background") },
    "ic-brand-Login-Content-inner-border" => lambda { I18n.t("Inner Box Border") },
    "ic-brand-Login-Content-inner-body-bgd" => lambda { I18n.t("Form Background") },
    "ic-brand-Login-Content-inner-body-border" => lambda { I18n.t("Form Border") },
    "ic-brand-Login-Content-label-text-color" => lambda { I18n.t("Login Label") },
    "ic-brand-Login-Content-password-text-color" => lambda { I18n.t("Login Link Color") },
    "ic-brand-Login-footer-link-color" => lambda { I18n.t("Login Footer Link") },
    "ic-brand-Login-footer-link-color-hover" => lambda { I18n.t("Login Footer Link Hover") },
    "ic-brand-Login-instructure-logo" => lambda { I18n.t("Login Instructure Logo") }
  }.freeze

  GROUP_NAMES = {
    "global_branding" => lambda { I18n.t("Global Branding") },
    "global_navigation" => lambda { I18n.t("Global Navigation") },
    "watermarks" => lambda { I18n.t("Watermarks & Other Images") },
    "login" => lambda { I18n.t("Login Screen") }
  }.freeze

  HELPER_TEXTS = {
    "ic-brand-header-image" => lambda { I18n.t("Accepted formats: svg, png, jpg, gif") },
    "ic-brand-mobile-global-nav-logo" => lambda { I18n.t("Appears at the top of the global navigation tray that opens on mobile sized screens. display height: 48px. Accepted formats: svg, png, jpg, gif") },
    "ic-brand-watermark" => lambda { I18n.t("This image appears as a background watermark to your page. Accepted formats: png, svg, gif, jpeg") },
    "ic-brand-watermark-opacity" => lambda { I18n.t("Specify the transparency of the watermark background image.") },
    "ic-brand-favicon" => lambda { I18n.t("You can use a single 16x16, 32x32, 48x48 ico file.") },
    "ic-brand-apple-touch-icon" => lambda { I18n.t("The shortcut icon for iOS/Android devices. 180x180 png") },
    "ic-brand-msapplication-tile-square" => lambda { I18n.t("558x558 png, jpg, gif (1.8x the standard tile size, so it can be scaled up or down as needed)") },
    "ic-brand-msapplication-tile-wide" => lambda { I18n.t("558x270 png, jpg, gif") },
    "ic-brand-right-sidebar-logo" => lambda { I18n.t("A full-size logo that appears in the right sidebar on the Canvas dashboard. Ideal size is 360 x 140 pixels. Accepted formats: svg, png, jpeg, gif") },
    "ic-brand-Login-body-bgd-shadow-color" => lambda { I18n.t("accepted formats: hex, rgba, rgb, hsl") }
  }.freeze

  class << self
    def variables_map
      @variables_map ||= BRANDABLE_VARIABLES.each_with_object({}) do |variable_group, memo|
        variable_group['variables'].each { |variable| memo[variable['variable_name']] = variable }
      end.freeze
    end

    def variables_map_with_image_urls
      @variables_map_with_image_urls ||= variables_map.each_with_object({}) do |(key, config), memo|
        if config['type'] == 'image'
          memo[key] = config.merge('default' => ActionController::Base.helpers.image_url(config['default']))
        else
          memo[key] = config
        end
      end.freeze
    end

    def things_that_go_into_defaults_md5
      variables_map.each_with_object({}) do |(variable_name, config), memo|
        default = config['default']
        if config['type'] == 'image'
          # to make consistent md5s whether the cdn is enabled or not, don't include hostname in defaults
          default = ActionController::Base.helpers.image_path(default, host: '')
        end
        memo[variable_name] = default
      end.freeze
    end

    def migration_version
      # ActiveRecord usually uses integer timestamps to generate migration versions but any integer
      # will work, so we just use the result of stripping out the alphabetic characters from the md5
      default_variables_md5_without_migration_check.gsub(/[a-z]/, '').to_i.freeze
    end

    def check_if_we_need_to_create_a_db_migration
      path = ActiveRecord::Migrator.migrations_paths.first
      args = [path]
      args << ActiveRecord::SchemaMigration unless CANVAS_RAILS5_2
      migrations = ActiveRecord::MigrationContext.new(*args).migrations
      ['predeploy', 'postdeploy'].each do |pre_or_post|
        migration = migrations.find { |m| m.name == MIGRATION_NAME + pre_or_post.camelize }
        # they can't have the same id, so we just add 1 to the postdeploy one
        expected_version = (pre_or_post == 'predeploy') ? migration_version : (migration_version + 1)
        raise BrandConfigWithOutCompileAssets if expected_version == 85663486644871658581990
        raise DefaultMD5NotUpToDateError unless migration && migration.version == expected_version
      end
    end

    def skip_migration_check?
      # our canvas_rspec build doesn't even run `yarn install` or `gulp rev` so since
      # they are not expecting all the frontend assets to work, this check isn't useful
      Rails.env.test? && !Rails.root.join('public', 'dist', 'rev-manifest.json').exist?
    end

    def default_variables_md5
      @default_variables_md5 ||= begin
        check_if_we_need_to_create_a_db_migration unless skip_migration_check?
        default_variables_md5_without_migration_check
      end
    end

    def default_variables_md5_without_migration_check
      Digest::MD5.hexdigest(things_that_go_into_defaults_md5.to_json).freeze
    end

    def handle_urls(value, config, css_urls)
      return value unless config['type'] == 'image' && css_urls
      "url('#{value}')" if value.present?
    end

    # gets the *effective* value for a brandable variable
    def brand_variable_value(variable_name, active_brand_config=nil, config_map=variables_map, css_urls=false)
      config = config_map[variable_name]
      explicit_value = active_brand_config && active_brand_config.get_value(variable_name).presence
      return handle_urls(explicit_value, config, css_urls) if explicit_value
      default = config['default']
      if default && default.starts_with?('$')
        if css_urls
          return "var(--#{default[1..-1]})"
        else
          return brand_variable_value(default[1..-1], active_brand_config, config_map, css_urls)
        end
      end

      # while in our sass, we want `url(/images/foo.png)`,
      # the Rails Asset Helpers expect us to not have the '/images/', eg: <%= image_tag('foo.png') %>
      default = default.sub(/^\/images\//, '') if config['type'] == 'image'
      handle_urls(default, config, css_urls)
    end

    def computed_variables(active_brand_config=nil)
      [
        ['ic-brand-primary', 'darken', 5],
        ['ic-brand-primary', 'darken', 10],
        ['ic-brand-primary', 'darken', 15],
        ['ic-brand-primary', 'lighten', 5],
        ['ic-brand-primary', 'lighten', 10],
        ['ic-brand-primary', 'lighten', 15],
        ['ic-brand-button--primary-bgd', 'darken', 5],
        ['ic-brand-button--primary-bgd', 'darken', 15],
        ['ic-brand-button--secondary-bgd', 'darken', 5],
        ['ic-brand-button--secondary-bgd', 'darken', 15],
        ['ic-brand-font-color-dark', 'lighten', 15],
        ['ic-brand-font-color-dark', 'lighten', 30],
        ['ic-link-color', 'darken', 10],
        ['ic-link-color', 'lighten', 10],
      ].each_with_object({}) do |(variable_name, darken_or_lighten, percent), memo|
        color = brand_variable_value(variable_name, active_brand_config, variables_map_with_image_urls)
        computed_color = CanvasColor::Color.new(color).send(darken_or_lighten, percent/100.0)
        memo["#{variable_name}-#{darken_or_lighten}ed-#{percent}"] = computed_color.to_s
      end
    end

    def all_brand_variable_values(active_brand_config=nil, css_urls=false)
      variables_map.each_with_object(computed_variables(active_brand_config)) do |(key, _), memo|
        memo[key] = brand_variable_value(key, active_brand_config, variables_map_with_image_urls, css_urls)
      end
    end

    def all_brand_variable_values_as_json(active_brand_config=nil)
      all_brand_variable_values(active_brand_config).to_json
    end

    def all_brand_variable_values_as_js(active_brand_config=nil)
      "CANVAS_ACTIVE_BRAND_VARIABLES = #{all_brand_variable_values_as_json(active_brand_config)};"
    end

    def all_brand_variable_values_as_css(active_brand_config=nil)
      ":root {
        #{all_brand_variable_values(active_brand_config, true).map{ |k, v| "--#{k}: #{v};"}.join("\n")}
      }"
    end

    def public_brandable_css_folder
      Pathname.new('public/dist/brandable_css')
    end

    def default_brand_folder
      public_brandable_css_folder.join('default')
    end

    def default_brand_file(type, high_contrast=false)
      default_brand_folder.join("variables#{high_contrast ? '-high_contrast' : ''}-#{default_variables_md5}.#{type}")
    end

    def high_contrast_overrides
      Class.new do
        def get_value(variable_name)
          {"ic-brand-primary" => "#0770A3", "ic-link-color" => "#0073A7"}[variable_name]
        end
      end.new
    end

    def default(type, high_contrast=false)
      bc = high_contrast ? high_contrast_overrides : nil
      send("all_brand_variable_values_as_#{type}", bc)
    end

    def save_default!(type, high_contrast=false)
      default_brand_folder.mkpath
      default_brand_file(type, high_contrast).write(default(type, high_contrast))
      move_default_to_s3_if_enabled!(type, high_contrast)
    end

    def save_default_files!
      [true, false].each do |high_contrast|
        ['js', 'css', 'json'].each { |type| save_default!(type, high_contrast) }
      end
    end

    def move_default_to_s3_if_enabled!(type, high_contrast=false)
      return unless defined?(Canvas) && Canvas::Cdn.enabled?
      s3_uploader.upload_file(public_default_path(type, high_contrast))
      begin
        File.delete(default_brand_file(type, high_contrast))
      rescue Errno::ENOENT # continue if something else deleted it in another process
      end
    end

    def s3_uploader
      @s3_uploaderer ||= Canvas::Cdn::S3Uploader.new
    end

    def public_default_path(type, high_contrast=false)
      "dist/brandable_css/default/variables#{high_contrast ? '-high_contrast' : ''}-#{default_variables_md5}.#{type}"
    end

    def variants
      @variants ||= CONFIG['variants'].keys.freeze
    end

    def brandable_variants
      @brandable_variants ||= CONFIG['variants'].select{|_, v| v['brandable']}.map{ |k,_| k }.freeze
    end

    def combined_checksums
      if defined?(ActionController) && ActionController::Base.perform_caching && defined?(@combined_checksums)
        return @combined_checksums
      end
      file = APP_ROOT.join(CONFIG['paths']['bundles_with_deps'] + SASS_STYLE)
      if file.exist?
        @combined_checksums = JSON.parse(file.read).each_with_object({}) do |(k, v), memo|
          memo[k] = v.symbolize_keys.slice(:combinedChecksum, :includesNoVariables)
        end.freeze
      elsif defined?(Rails) && Rails.env.production?
        raise "#{file.expand_path} does not exist. You need to run brandable_css before you can serve css."
      else
        # for dev/test there might be cases where you don't want it to raise an exception
        # if you haven't ran `brandable_css` and the manifest file doesn't exist yet.
        # eg: you want to test a controller action and you don't care that it links
        # to a css file that hasn't been created yet.
        default_value = {:combinedChecksum => "Error: unknown css checksum. you need to run brandable_css"}.freeze
        @combined_checksums = Hash.new(default_value).freeze
      end
    end

    # bundle path should be something like "bundles/speedgrader" or "plugins/analytics/something"
    def cache_for(bundle_path, variant)
      key = ["#{bundle_path}.scss", variant].join(CONFIG['manifest_key_seperator'])
      fingerprint = combined_checksums[key]
      raise "Fingerprint not found. #{bundle_path} #{variant}" unless fingerprint
      fingerprint
    end

    def all_fingerprints_for(bundle_path)
      variants.each_with_object({}) do |variant, object|
        object[variant] = cache_for(bundle_path, variant)
      end
    end
  end

  class BrandConfigWithOutCompileAssets < RuntimeError
    def initialize
      super <<-END

It looks like you are running a migration before running `rake canvas:compile_assets`
compile_assets needs to complete before running db:migrate if brand_configs have not run

run `rake canvas:compile_assets` and then try migrations again.

      END
    end
  end

  class DefaultMD5NotUpToDateError < RuntimeError
    def initialize
      super <<-END

Something has changed about the default variables or images used in the Theme Editor.
If you are seeing this and _you_ did not make changes to either app/stylesheets/brandable_variables.json
or one of the images it references, it probably meeans your local setup is out of date.

First, make sure you run `rake db:migrate`
and then run `./script/nuke_node.sh`

If that does not resolve the issue, it probably means you _did_ update one of those json variables
in app/stylesheets/brandable_variables.json or one of the images it references so you need to rename
the db migrations that makes sure when this change is deployed or checked out by anyone else
makes a new .css file for the css variables for each brand based on these new defaults.
To do that, run this command and then restart your rails process. (for local dev, if you want the
changes to show up in the ui, make sure you also run `rake db:migrate` afterwards).

ONLY DO THIS IF YOU REALLY DID MEAN TO MAKE A CHANGE TO THE DEFAULT BRANDING STUFF:

mv db/migrate/*_#{MIGRATION_NAME.underscore}_predeploy.rb \\
   db/migrate/#{BrandableCSS.migration_version}_#{MIGRATION_NAME.underscore}_predeploy.rb \\
&& \\
mv db/migrate/*_#{MIGRATION_NAME.underscore}_postdeploy.rb \\
   db/migrate/#{BrandableCSS.migration_version + 1}_#{MIGRATION_NAME.underscore}_postdeploy.rb

FYI, current variables are: #{BrandableCSS.things_that_go_into_defaults_md5}
END
    end
  end
end
