# frozen_string_literal: true

# MIT License
#
# Copyright (c) 2021 package-url
# Portions Copyright 2022 Gitlab B.V.
#
# 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 Sbom
  class PackageUrl
    class Encoder
      include StringUtils

      def initialize(package)
        @type = package.type
        @namespace = package.namespace
        @name = package.name
        @version = package.version
        @qualifiers = package.qualifiers
        @subpath = package.subpath
        @io = StringIO.new
      end

      def encode
        encode_scheme!
        encode_type!
        encode_name!
        encode_version!
        encode_qualifiers!
        encode_subpath!

        io.string
      end

      private

      attr_reader :io

      def encode_scheme!
        io.write('pkg:')
      end

      def encode_type!
        # Append the type string to the purl as a lowercase ASCII string
        # Append '/' to the purl
        io.write(@type)
        io.write('/')
      end

      def encode_name!
        # If the namespace is empty:
        # - Apply type-specific normalization to the name if needed
        # - UTF-8-encode the name if needed in your programming language
        # - Append the percent-encoded name to the purl
        #
        # If the namespace is not empty:
        # - Strip the namespace from leading and trailing '/'
        # - Split on '/' as segments
        # - Apply type-specific normalization to each segment if needed
        # - UTF-8-encode each segment if needed in your programming language
        # - Percent-encode each segment
        # - Join the segments with '/'
        # - Append this to the purl
        # - Append '/' to the purl
        # - Strip the name from leading and trailing '/'
        # - Apply type-specific normalization to the name if needed
        # - UTF-8-encode the name if needed in your programming language
        # - Append the percent-encoded name to the purl
        if @namespace.nil?
          io.write(URI.encode_www_form_component(@name, Encoding::UTF_8))
        else
          io.write(encode_segments(@namespace, &:empty?))
          io.write('/')
          io.write(URI.encode_www_form_component(strip(@name, '/'), Encoding::UTF_8))
        end
      end

      def encode_version!
        return if @version.nil?

        # - Append '@' to the purl
        # - UTF-8-encode the version if needed in your programming language
        # - Append the percent-encoded version to the purl
        io.write('@')
        io.write(URI.encode_www_form_component(@version, Encoding::UTF_8))
      end

      def encode_qualifiers!
        return if @qualifiers.nil? || encoded_qualifiers.empty?

        io.write('?')
        io.write(encoded_qualifiers)
      end

      def encoded_qualifiers
        @encoded_qualifiers ||= @qualifiers.filter_map do |key, value|
          next if value.empty?

          next "#{key.downcase}=#{value.join(',')}" if key == 'checksums' && value.is_a?(::Array)

          "#{key.downcase}=#{URI.encode_www_form_component(value, Encoding::UTF_8)}"
        end.sort.join('&')
      end

      def encode_subpath!
        return if @subpath.nil? || encoded_subpath.empty?

        io.write('#')
        io.write(encoded_subpath)
      end

      def encoded_subpath
        @encoded_subpath ||= encode_segments(@subpath) do |segment|
          # Discard segments which are blank, `.`, or `..`
          segment.empty? || segment == '.' || segment == '..'
        end
      end
    end
  end
end