# taken from https://github.com/benbalter/jekyll-relative-links/tree/master/lib/jekyll-relative-links

# normally we'd add it to Gemfile and include { plugins: [ jekyll-relative-links ] } in the config.yml
# but the last released version 0.6.1 is too old and doesn't support absolute links, so we copy head here instead

# additional changes are in commit history
# - speculatively map .html to .md when doing the lookup
# - make url_for_path public, and have way to inject site and initialize context outside of normal generator usage

# distributed under the MIT License as follows (note this is only used to build the docs, not included with any Brooklyn output):

# MIT License
#
# Copyright (c) 2016 Ben Balter
# 
# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


module JekyllRelativeLinks
  class Context
    attr_reader :site

    def initialize(site)
      @site = site
    end

    def registers
      { :site => site }
    end
  end

  class Generator < Jekyll::Generator
    attr_accessor :site, :config

    # Use Jekyll's native relative_url filter
    include Jekyll::Filters::URLFilters

    LINK_TEXT_REGEX = %r!(.*?)!.freeze
    FRAGMENT_REGEX = %r!(#.+?|)?!.freeze
    TITLE_REGEX = %r{(\s+"(?:\\"|[^"])*(?<!\\)"|\s+"(?:\\'|[^'])*(?<!\\)')?}.freeze
    FRAG_AND_TITLE_REGEX = %r!#{FRAGMENT_REGEX}#{TITLE_REGEX}!.freeze
    INLINE_LINK_REGEX = %r!\[#{LINK_TEXT_REGEX}\]\(([^\)]*?)#{FRAG_AND_TITLE_REGEX}\)!.freeze
    REFERENCE_LINK_REGEX = %r!^\s*?\[#{LINK_TEXT_REGEX}\]: (.+?)#{FRAG_AND_TITLE_REGEX}\s*?$!.freeze
    LINK_REGEX = %r!(#{INLINE_LINK_REGEX}|#{REFERENCE_LINK_REGEX})!.freeze
    CONVERTER_CLASS = Jekyll::Converters::Markdown
    CONFIG_KEY = "relative_links"
    ENABLED_KEY = "enabled"
    COLLECTIONS_KEY = "collections"
    LOG_KEY = "Relative Links:"

    safe true
    priority :lowest

    # warnings can be misleading e.g. if the link is in an excluded liquid tag; but might be useful in places
    # for most things, use eg htmlproofer on the output site
    @warn = false

    def initialize(config)
      @config = config
    end

    def prepare_for_site(site)
      @potential_targets = nil
      @site = site
      @context = context
    end

    def warn_missing_links(warn)
      @warn = warn
    end

    def generate(site)
      return if disabled?

      @site    = site
      @context = context

      documents = site.pages
      documents = site.pages + site.docs_to_write if collections?

      documents.each do |document|
        next unless markdown_extension?(document.extname)
        next if document.is_a?(Jekyll::StaticFile)
        next if excluded?(document)

        replace_relative_links!(document)
      end
    end


    def replace_relative_links!(document)
      return document if document.content.nil?

      document.content = replace_relative_links_in_content(document.content, document.relative_path)

      replace_relative_links_excerpt!(document)
    rescue ArgumentError => e
      raise e unless e.to_s.start_with?("invalid byte sequence in UTF-8")
    end

    def replace_relative_links_in_content(content, relative_to_path)
      url_base = File.dirname(relative_to_path)

      content.dup.gsub(LINK_REGEX) do |original|
        link = link_parts(Regexp.last_match)

        if (link.path == "" && link.fragment == "" && link.text && link.text.start_with?("http"))
          link.path = link.text

        else
          next original unless replaceable_link?(link.path)

          path = path_from_root(link.path, url_base)
          url  = url_for_path(path, relative_to_path)

          next original unless url

          link.path = url
        end
        replacement_text(link)
      end
    end

    def url_for_path_absolute(path)
      is_absolute = path.start_with? "/"
      is_section = path.include? "#"
      fragment = path.sub(/[^#]*#/, "") if is_section
      path = path.sub(/#.*/,"") if is_section

      path = path.sub(%r!\A/!, "")
      # puts "lookup #{path} / #{path.sub(%r!\.html!.freeze, ".md")}"
      url = url_for_path_internal(path)
      # also try html and / mapped to md - useful if using a baseurl
      url = url_for_path_internal(path.sub(%r!\.html!.freeze, ".md")) unless url
      url = url_for_path_internal(path.sub(%r!\.md!.freeze, ".html")) unless url
      url = url_for_path_internal(path.sub(%r!/\z!.freeze, "") + "/index.md") unless url
      url = url_for_path_internal(path.sub(%r!/\z!.freeze, "") + "/index.html") unless url
      url = "/" + url if url && is_absolute && !url.start_with?("/")
      url = "#{url}##{fragment}" if is_section
      url
    end

    private

    def url_for_path(path, src)
      path.sub!(%r!\A/!, "")
      # puts "lookup #{path} / #{path.sub(%r!\.html!.freeze, ".md")}"
      url = url_for_path_internal(path)

      pathWithText = %r!^(.*\.(png|jpg|gif|svg))( .*)\z!.freeze.match(path)
      # don't match images
      if (!pathWithText)
        # try to find it with .html suffix or if path to folder with or without / or if extension omitted
        url = url_for_path_internal(path.sub(%r!\.html!.freeze, ".md")) unless url
        url = url_for_path_internal(path.sub(%r!/\z!.freeze, "") + "/index.md") unless url
        url = url_for_path_internal(path.sub(%r!\z!.freeze, "") + "/index.md") unless url
        url = url_for_path_internal(path.sub(%r!\z!.freeze, ".md")) unless url
        puts "WARN: unresolved link in #{src}:  #{path}" unless url if @warn

      else
        url = url_for_path(pathWithText[1], src) if pathWithText
        url = url + pathWithText[3] if url
        url = path unless url
      end
      url
    end

    def url_for_path_internal(path)
      path = path.sub(%r!\A/!, "")
      path = CGI.unescape(path)
      target = potential_targets.find { |p| p.relative_path.sub(%r!\A/!, "") == path }
      relative_url(target.url) if target&.url
    end

    # Stores info on a Markdown Link (avoid rubocop's Metrics/ParameterLists warning)
    Link = Struct.new(:link_type, :text, :path, :fragment, :title)

    def link_parts(matches)
      last_inline = 5
      link_type = matches[2] ? :inline : :reference
      link_text = matches[link_type == :inline ? 2 : last_inline + 1]
      relative_path = matches[link_type == :inline ? 3 : last_inline + 2]
      fragment = matches[link_type == :inline ? 4 : last_inline + 3]
      title = matches[link_type == :inline ? 5 : last_inline + 4]
      Link.new(link_type, link_text, relative_path, fragment, title)
    end

    def context
      @context ||= JekyllRelativeLinks::Context.new(site)
    end

    def markdown_extension?(extension)
      markdown_converter.matches(extension)
    end

    def markdown_converter
      @markdown_converter ||= site.find_converter_instance(CONVERTER_CLASS)
    end

    def potential_targets
      @potential_targets ||= site.pages + site.static_files + site.docs_to_write
    end

    def path_from_root(relative_path, url_base)
      is_absolute = relative_path.start_with? "/"

      relative_path.sub!(%r!\A/!, "")
      base = is_absolute ? "" : url_base
      absolute_path = File.expand_path(relative_path, base)
      absolute_path.sub(%r!\A#{Regexp.escape(Dir.pwd)}/!, "")
    end

    # @param link [Link] A Link object describing the markdown link to make
    def replacement_text(link)
      link.path << link.fragment if link.fragment

      if link.link_type == :inline
        "[#{link.text}](#{link.path}#{link.title})"
      else
        "\n[#{link.text}]: #{link.path}#{link.title}"
      end
    end

    def absolute_url?(string)
      return unless string

      Addressable::URI.parse(string).absolute?
    rescue Addressable::URI::InvalidURIError
      nil
    end

    def fragment?(string)
      string&.start_with?("#")
    end

    def replaceable_link?(string)
      !fragment?(string) && !absolute_url?(string) && string != ""
    end

    def option(key)
      config[CONFIG_KEY] && config[CONFIG_KEY][key]
    end

    def disabled?
      option(ENABLED_KEY) == false
    end

    def collections?
      option(COLLECTIONS_KEY) == true
    end

    def excluded?(document)
      return false unless option("exclude")

      entry_filter = if document.respond_to?(:collection)
                       document.collection.entry_filter
                     else
                       global_entry_filter
                     end

      entry_filter.glob_include?(option("exclude"), document.relative_path).tap do |excluded|
        Jekyll.logger.debug(LOG_KEY, "excluded #{document.relative_path}") if excluded
      end
    end

    def global_entry_filter
      @global_entry_filter ||= Jekyll::EntryFilter.new(site)
    end

    def replace_relative_links_excerpt!(document)
      document.data["excerpt"] = Jekyll::Excerpt.new(document) if document.data["excerpt"]
    end
  end
end

