# This plugin enriches the product pages by setting or precomputing its metadata, so that it can be
# easily consumed in layouts or plugins (such as the API v1 plugin).
#
# Naming conventions:
# - Raw fields, declared in product's markdown front matter or derived from a template (such as the
#   changelogTemplate), use the camel case notation (example: endOfLife).
# - Computed fields, injected by ProductDataEnricher, use the snake case notation (example: end_of_life).
#
# Here is a list of computed fields :
# - is_maintained (in cycles) : whether the release cycle is still supported (mandatory)
# - can_be_hidden (in cycles) : whether the release cycle can be hidden when displaying the product page (mandatory)
# - is_lts (in cycles) : whether the release cycle is currently in its LTS phase (mandatory)
# - lts_from (in cycles) : the LTS phase start date for the release cycle (optional, only if lts is a Date)
# - is_eoas (in cycles) : whether the release cycle has reach the end of active support (optional, only if eoas is set)
# - is_almost_eoas (in cycles) : whether the release cycle will soon reach the end of active support (optional, only if eoas is set to a Date)
# - eoas_from (in cycles) : end of the release cycle active support phase date (optional, only if eoas is set to a Date)
# - is_eol (in cycles) : whether the release cycle is currently eol (optional, only if eol is set)
# - is_almost_eol (in cycles) : whether the release cycle will soon reach eol (optional, only if eol is set to a Date)
# - eol_from (in cycles) : EOL date of the release cycle (optional, only if eol is set to a Date)
# - is_discontinued (in cycles) : whether the release cycle is currently discontinued (optional, only if discontinued is set)
# - is_almost_discontinued (in cycles) : whether the release cycle will soon be discontinued (optional, only if discontinued is set to a Date)
# - discontinued_from (in cycles) : discontinuation date of the release cycle (optional, only if discontinued is set to a Date)
# - is_eoes (in cycles) : whether the release cycle has reach the end of extended support (optional, only if eoes is set)
# - is_almost_eoes (in cycles) : whether the release cycle will soon reach the end of extended support (optional, only if eoes is set to a Date)
# - eoes_from (in cycles) : end of the release cycle extended support phase date (optional, only if eoes is set to a Date)

require_relative 'end-of-life'
require_relative 'identifier-to-url'

module Jekyll
  class ProductDataEnricher
    class << self

      TOPIC = "EndOfLife Product Data Enricher:"

      def enrich(page)
        Jekyll.logger.debug TOPIC, "Enriching #{page.name}"

        set_id(page)
        set_description(page)
        set_icon_url(page)
        set_parent(page)
        set_tags(page)
        set_identifiers_url(page)
        set_aliases(page)
        set_overridden_columns_label(page)

        page.data["releases"].each { |release| enrich_release(page, release) }

        # DO NOT MOVE : below methods need information computed by enrich_release.
        flag_oldest_unmaintained_releases(page)
      end

      def is_product?(page)
        page.data['layout'] == 'product'
      end

      private

      # Build the product id from the permalink.
      def set_id(page)
        page.data['id'] = page.data['permalink'][1..page.data['permalink'].length]
      end

      # Build the product description, if it's not already set in the product's front matter.
      def set_description(page)
        unless page.data['description']
          page.data['description'] = "Check end-of-life, release policy and support schedule for #{page.data['title']}."
        end
      end

      # Build the icon URL from the icon slug.
      def set_icon_url(page)
        if page['iconSlug']
          page.data['iconUrl'] = "https://cdn.jsdelivr.net/npm/simple-icons/icons/#{page['iconSlug']}.svg"
        end
      end

      # Set the parent page for navigation.
      def set_parent(page)
        page.data['parent'] = tag_title(page.data['category'])
      end

      # Explode tags space-separated string to a list if necessary.
      # Also add the category as a default tag.
      def set_tags(page)
        tags = page.data['tags']

        if tags
          tags = (tags.kind_of?(Array) ? tags : tags.split)
        else
          tags = []
        end

        tags << page.data['category']
        page.data['tags'] = tags.sort
      end

      # Set alias (derived from alternate_urls).
      def set_aliases(page)
        if page.data['alternate_urls']
          page.data['aliases'] = page.data['alternate_urls'].map { |path| path[1..] }
        else
          page.data['alternate_urls'] = [] # should be in a separate method, but easier that way
          page.data['aliases'] = []
        end
      end

      # Set each identifiers URL.
      def set_identifiers_url(page)
        for identifier in page.data['identifiers']
          unless identifier['url']
            identifier['url'] = IdentifierToUrl.new.render(identifier)
          end
        end
      end

      # Set properly the column presence/label if it was overridden.
      def set_overridden_columns_label(page)
        date_column_names = %w[releaseDateColumn latestColumn discontinuedColumn eoasColumn eolColumn eoesColumn]
        date_column_names.each { |date_column|
          if page.data[date_column].is_a? String
            page.data[date_column + 'Label'] = page.data[date_column]
            page.data[date_column] = true
          end
        }
      end

      # Flag all cycles that can be hidden (see #50).
      #
      # The goal of this function is to hide only a single run of rows, at the very end, if they are
      # all unmaintained. This function presume that all cycles are ordered by their release date,
      # so a cycle can be hidden only if:
      # - it is not the first cycle,
      # - it is unmaintained (see set_is_maintained below),
      # - the previous cycle is still maintained,
      # - all next cycles are unmaintained.
      #
      # This function applies only if there are more than 6 cycles and more than 2 cycles that can
      # be hidden.
      #
      # For example, given there are 10 cycles with various state of maintainability:
      # - cycle 1 to 3 are maintained => cannot be hidden because they are maintained.
      # - cycle 4 is unmaintained => cannot be hidden because cycle 5 is maintained.
      # - cycle 5 is maintained => cannot be hidden because it is maintained.
      # - cycle 6 is unmaintained => cannot be hidden because cycle 5 is maintained.
      # - cycle 7 to 10 are unmaintained => can be hidden.
      def flag_oldest_unmaintained_releases(page)
        min_total_cycles = 6 # apply only if the number of cycles is greater than this
        min_hidden_cycles = 3 # apply only if the number of hidden cycles is greater than this (must be < min_total_cycles)

        releases = page.data['releases']
        if releases.length <= min_total_cycles
          Jekyll.logger.debug TOPIC, "Less than #{min_total_cycles} cycles on #{page.name}, will not try to hide cycles"
          return
        end

        hidden_cycles = mark_cycles_that_can_be_hidden(releases)

        if releases[0]['can_be_hidden']
          Jekyll.logger.debug TOPIC, "First cycle is hidden on #{page.name}, unhide cycle"
          releases[0].delete('can_be_hidden')
          hidden_cycles.delete(releases[0])
        end

        if hidden_cycles.length > 0 and hidden_cycles.length < min_hidden_cycles
          Jekyll.logger.debug TOPIC, "Less than #{min_hidden_cycles} hidden cycles on #{page.name}, unhide #{hidden_cycles.length} cycles"
          hidden_cycles.each { |cycle| cycle.delete('can_be_hidden') }
          hidden_cycles.clear
        end

        Jekyll.logger.debug TOPIC, "Hide #{hidden_cycles.length} cycles on #{page.name}"
      end

      def mark_cycles_that_can_be_hidden(ordered_by_date_desc_releases)
        hidden_cycles = []
        previous_cycle = nil

        ordered_by_date_desc_releases.reverse.each { |cycle|
          if not cycle['is_maintained']
            if previous_cycle
              previous_cycle['can_be_hidden'] = true
              hidden_cycles << previous_cycle
            end

            previous_cycle = cycle
          else
            break
          end
        }

        return hidden_cycles
      end

      def enrich_release(page, cycle)
        set_cycle_id(cycle)
        set_cycle_lts_fields(cycle)
        set_cycle_eoas_fields(cycle)
        set_cycle_eoes_fields(cycle)
        set_cycle_eol_fields(cycle)
        set_cycle_discontinued_fields(cycle)
        set_cycle_link(page, cycle)
        set_cycle_label(page, cycle)
        add_lts_label_to_cycle_label(page, cycle) # must be called after set_cycle_lts
        set_is_maintained(cycle) # must be called after set_cycle_*_fields
      end

      # Build the cycle id from the permalink.
      def set_cycle_id(cycle)
        cycle['id'] = cycle['releaseCycle'].tr('/', '-')
      end

      # Set lts to false if it has no value and explode it to is_lts (boolean) and lts_from (Date).
      # See explode_date_or_boolean_field(...) for more information.
      def set_cycle_lts_fields(cycle)
        unless cycle.has_key?('lts')
          cycle['lts'] = false
        end

        explode_date_or_boolean_field(cycle, 'lts', 'is_lts', 'lts_from')
      end

      # Explode eoas to is_eoas (boolean) and eoas_from (Date).
      # See explode_date_or_boolean_field(...) for more information.
      def set_cycle_eoas_fields(cycle)
        explode_date_or_boolean_field(cycle, 'eoas', 'is_eoas', 'eoas_from')
        compute_almost_field(cycle, 'eoas', 'is_almost_eoas')
      end

      # Explode eoes to is_eoes (boolean) and eoes_from (Date).
      # See explode_date_or_boolean_field(...) for more information.
      def set_cycle_eoes_fields(cycle)
        explode_date_or_boolean_field(cycle, 'eoes', 'is_eoes', 'eoes_from')
        compute_almost_field(cycle, 'eoes', 'is_almost_eoes')
      end

      # Explode eol to is_eol (boolean) and eol_from (Date).
      # See explode_date_or_boolean_field(...) for more information.
      def set_cycle_eol_fields(cycle)
        explode_date_or_boolean_field(cycle, 'eol', 'is_eol', 'eol_from')
        compute_almost_field(cycle, 'eol', 'is_almost_eol')
      end

      # Explode discontinued to is_discontinued (boolean) and discontinued_from (Date).
      # See explode_date_or_boolean_field(...) for more information.
      def set_cycle_discontinued_fields(cycle)
        explode_date_or_boolean_field(cycle, 'discontinued', 'is_discontinued', 'discontinued_from')
        compute_almost_field(cycle, 'discontinued', 'is_almost_discontinued')
      end

      # Some release cycle fields (field_name) can be either a date or a boolean.
      # This function create two additional variables, one of boolean type (boolean_field_name) and
      # the other of Date type (date_field_name) to simplify usages in templates or Jekyll plugins.
      #
      # The invert parameter must be set according to the date nature. If it's a start date
      # (example : the eol field) set it to false, if it's an end date (example : the eoas field)
      # set it to true.
      def explode_date_or_boolean_field(cycle, field_name, boolean_field_name, date_field_name)
        unless cycle.has_key?(field_name)
          return
        end

        value = cycle[field_name]
        if value.is_a?(Date)
          cycle[boolean_field_name] = (Date.today > value)
          cycle[date_field_name] = value
        else
          cycle[boolean_field_name] = value
          cycle[date_field_name] = nil
        end
      end

      # Compute the almost_field_name field.
      def compute_almost_field(cycle, field_name, almost_field_name)
        field_value = cycle[field_name]
        unless field_value.is_a?(Date)
          return
        end

        period_start = cycle['releaseDate'].to_time.to_i # release at midnight
        period_end = field_value.to_time.to_i # eoas/eol/eoes at midnight
        now = Date.today.to_time.to_i # today at midnight
        time_until_end = period_end - now

        max_threshold = 4 * 30 * 24 * 60 * 60 # 4 months in seconds
        threshold = [(period_end - period_start) / 3, max_threshold].min
        is_almost_at_end = (0..threshold).include?(time_until_end)

        cycle[almost_field_name] = is_almost_at_end
      end

      def set_cycle_link(page, cycle)
        if cycle.has_key?('link')
          # null link means no changelog template
          if cycle['link'] && cycle['link'].include?('__')
            cycle['link'] = render_eol_template(cycle['link'], cycle)
          end
        else
          if page['changelogTemplate']
            cycle['link'] = render_eol_template(page['changelogTemplate'], cycle)
          end
        end
      end

      def set_cycle_label(page, cycle)
        template = cycle['releaseLabel'] || page.data['releaseLabel']
        if template
          cycle['label'] = render_eol_template(template, cycle)
        else
          cycle['label'] = cycle['releaseCycle']
        end
      end

      def add_lts_label_to_cycle_label(page, cycle)
        lts_label = page.data['LTSLabel']

        if cycle['lts_from']
          if cycle['is_lts']
            cycle['label'] = "#{cycle['label']} (#{lts_label})"
          else
            cycle['label'] = "#{cycle['label']} (<span title=\"#{cycle['lts_from'].iso8601}\">Upcoming</span> #{lts_label})"
          end
        elsif cycle['is_lts']
          cycle['label'] = "#{cycle['label']} (#{lts_label})"
        end
      end

      # Compute whether the cycle is still maintained and add the result to the cycle's data.
      #
      # A cycle is maintained if at least one of the eoas / eol / discontinued / eoes dates
      # is in the future or is true.
      #
      # This function must be executed after all other field have been computed.
      def set_is_maintained(cycle)
        is_maintained = false

        %w[is_eoas is_eol is_discontinued is_eoes].each { |field|
          if cycle.has_key?(field) and not cycle[field]
            is_maintained = true
            break
          end
        }

        cycle['is_maintained'] = is_maintained
      end

      # Template rendering function that replaces placeholders.
      # The template is stripped to avoid unnecessary whitespaces in the output.
      def render_eol_template(template, cycle)
        link = template.strip().gsub('__RELEASE_CYCLE__', cycle['releaseCycle'] || '')
        link.gsub!('__CODENAME__', cycle['codename'] || '')
        link.gsub!('__RELEASE_DATE__', cycle['releaseDate'].iso8601)
        link.gsub!('__LATEST__', cycle['latest'] || '')
        link.gsub!('__LATEST_RELEASE_DATE__', cycle['latestReleaseDate'] ? cycle['latestReleaseDate'].iso8601 : '')
        return Liquid::Template.parse(link).render(@context)
      end
    end
  end
end

Jekyll::Hooks.register [:pages], :post_init, priority: Jekyll::Hooks::PRIORITY_MAP[:normal] do |page|
  Jekyll::ProductDataEnricher.enrich(page) if Jekyll::ProductDataEnricher.is_product?(page)
end
