/*
 * Copyright 2020 National Library of Norway.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package url

import "golang.org/x/text/encoding/charmap"

var defaultSpecialSchemes = map[string]string{
	"ftp":   "21",
	"file":  "",
	"http":  "80",
	"https": "443",
	"ws":    "80",
	"wss":   "443",
}

// parserOptions configure a url parser. parserOptions are set by the ParserOption
// values passed to NewParser.
type parserOptions struct {
	reportValidationErrors              bool
	failOnValidationError               bool
	laxHostParsing                      bool
	collapseConsecutiveSlashes          bool
	acceptInvalidCodepoints             bool
	preParseHostFunc                    func(url *Url, host string) string
	postParseHostFunc                   func(url *Url, host string) string
	percentEncodeSinglePercentSign      bool
	allowSettingPathForNonBaseUrl       bool
	skipWindowsDriveLetterNormalization bool
	specialSchemes                      map[string]string
	skipTrailingSlashNormalization      bool
	encodingOverride                    *charmap.Charmap
	pathPercentEncodeSet                *PercentEncodeSet
	specialQueryPercentEncodeSet        *PercentEncodeSet
	queryPercentEncodeSet               *PercentEncodeSet
	specialFragmentPercentEncodeSet     *PercentEncodeSet
	fragmentPercentEncodeSet            *PercentEncodeSet
}

// ParserOption configures how we parse a URL.
type ParserOption interface {
	apply(*parserOptions)
}

// EmptyParserOption does not alter the parser configuration. It can be embedded in
// another structure to build custom parser options.
type EmptyParserOption struct{}

func (EmptyParserOption) apply(*parserOptions) {}

// funcParserOption wraps a function that modifies parserOptions into an
// implementation of the ParserOption interface.
type funcParserOption struct {
	f func(*parserOptions)
}

func (fpo *funcParserOption) apply(po *parserOptions) {
	fpo.f(po)
}

func newFuncParserOption(f func(*parserOptions)) *funcParserOption {
	return &funcParserOption{
		f: f,
	}
}

func defaultParserOptions() parserOptions {
	return parserOptions{
		pathPercentEncodeSet:            PathPercentEncodeSet,
		specialQueryPercentEncodeSet:    SpecialQueryPercentEncodeSet,
		queryPercentEncodeSet:           QueryPercentEncodeSet,
		specialFragmentPercentEncodeSet: FragmentPercentEncodeSet,
		fragmentPercentEncodeSet:        FragmentPercentEncodeSet,
		specialSchemes:                  defaultSpecialSchemes,
	}
}

// WithReportValidationErrors records all non fatal validation errors so that they can be fetchd by a call to....
func WithReportValidationErrors() ParserOption {
	return newFuncParserOption(func(o *parserOptions) {
		o.reportValidationErrors = true
	})
}

// WithFailOnValidationError makes the parser throw an error on non fatal validation errors.
func WithFailOnValidationError() ParserOption {
	return newFuncParserOption(func(o *parserOptions) {
		o.failOnValidationError = true
	})
}

// WithLaxHostParsing ignores some decoding errors and returns the host as is.
//
// This API is EXPERIMENTAL.
func WithLaxHostParsing() ParserOption {
	return newFuncParserOption(func(o *parserOptions) {
		o.laxHostParsing = true
	})
}

// WithCollapseConsecutiveSlashes collapses consecutive slashes in path into one
// (e.g. http://example.com//foo///bar => http://example.com/foo/bar).
func WithCollapseConsecutiveSlashes() ParserOption {
	return newFuncParserOption(func(o *parserOptions) {
		o.collapseConsecutiveSlashes = true
	})
}

// WithAcceptInvalidCodepoints percent encodes values which are not valid UTF-8.
//
// This API is EXPERIMENTAL.
func WithAcceptInvalidCodepoints() ParserOption {
	return newFuncParserOption(func(o *parserOptions) {
		o.acceptInvalidCodepoints = true
	})
}

// WithPreParseHostFunc is a function which allows manipulation of host string before it is parsed.
//
// This API is EXPERIMENTAL.
func WithPreParseHostFunc(f func(url *Url, host string) string) ParserOption {
	return newFuncParserOption(func(o *parserOptions) {
		o.preParseHostFunc = f
	})
}

// WithPostParseHostFunc is a function which allows manipulation of host string after it is parsed.
// It is called only if the host isn't an IP address.
//
// This API is EXPERIMENTAL.
func WithPostParseHostFunc(f func(url *Url, host string) string) ParserOption {
	return newFuncParserOption(func(o *parserOptions) {
		o.postParseHostFunc = f
	})
}

// WithPercentEncodeSinglePercentSign percent encodes a '%' which is not followed by two hexadecimal digits
// instead of complaining about invalid percent encoding.
//
// This API is EXPERIMENTAL.
func WithPercentEncodeSinglePercentSign() ParserOption {
	return newFuncParserOption(func(o *parserOptions) {
		o.percentEncodeSinglePercentSign = true
	})
}

// WithAllowSettingPathForNonBaseUrl allows to set path for a url which cannot be a base url.
// WhathWg standard says this should be illegal
//
// This API is EXPERIMENTAL.
func WithAllowSettingPathForNonBaseUrl() ParserOption {
	return newFuncParserOption(func(o *parserOptions) {
		o.allowSettingPathForNonBaseUrl = true
	})
}

// WithSkipWindowsDriveLetterNormalization skips conversion of 'C|' to 'C:'.
// WhathWg standard says only a normalized Windows drive letter is conforming.
//
// This API is EXPERIMENTAL.
func WithSkipWindowsDriveLetterNormalization() ParserOption {
	return newFuncParserOption(func(o *parserOptions) {
		o.skipWindowsDriveLetterNormalization = true
	})
}

// WithSpecialSchemes allows overriding the notion of special schemes.
// special is a map of 'scheme' => 'default port'
//
// WhatWg standard removed gopher from special schemes. This is how you add it back:
// special := map[string]string{
//                        "ftp":    "21",
//                        "file":   "",
//                        "http":   "80",
//                        "https":  "443",
//                        "ws":     "80",
//                        "wss":    "443",
//                        "gopher": "70",
//                }
//
// This API is EXPERIMENTAL.
func WithSpecialSchemes(special map[string]string) ParserOption {
	return newFuncParserOption(func(o *parserOptions) {
		o.specialSchemes = special
	})
}

// WithEncodingOverride allows to set an encoding other than UTF-8 when parsing.
//
// This API is EXPERIMENTAL.
func WithEncodingOverride(cm *charmap.Charmap) ParserOption {
	return newFuncParserOption(func(o *parserOptions) {
		o.encodingOverride = cm
	})
}

// WithPathPercentEncodeSet allows to set an alternative set of characters to percent encode in path component.
//
// This API is EXPERIMENTAL.
func WithPathPercentEncodeSet(encodeSet *PercentEncodeSet) ParserOption {
	return newFuncParserOption(func(o *parserOptions) {
		o.pathPercentEncodeSet = encodeSet
	})
}

// WithQueryPercentEncodeSet allows to set an alternative set of characters to percent encode in query component
// when scheme is not special.
//
// This API is EXPERIMENTAL.
func WithQueryPercentEncodeSet(encodeSet *PercentEncodeSet) ParserOption {
	return newFuncParserOption(func(o *parserOptions) {
		o.queryPercentEncodeSet = encodeSet
	})
}

// WithSpecialQueryPercentEncodeSet allows to set an alternative set of characters to percent encode in query component
// when scheme is special.
//
// This API is EXPERIMENTAL.
func WithSpecialQueryPercentEncodeSet(encodeSet *PercentEncodeSet) ParserOption {
	return newFuncParserOption(func(o *parserOptions) {
		o.specialQueryPercentEncodeSet = encodeSet
	})
}

// WithFragmentPathPercentEncodeSet allows to set an alternative set of characters to percent encode in fragment
// component when scheme is not special.
//
// This API is EXPERIMENTAL.
func WithFragmentPathPercentEncodeSet(encodeSet *PercentEncodeSet) ParserOption {
	return newFuncParserOption(func(o *parserOptions) {
		o.fragmentPercentEncodeSet = encodeSet
	})
}

// WithSpecialFragmentPathPercentEncodeSet allows to set an alternative set of characters to percent encode in fragment
// component when scheme is special.
//
// This API is EXPERIMENTAL.
func WithSpecialFragmentPathPercentEncodeSet(encodeSet *PercentEncodeSet) ParserOption {
	return newFuncParserOption(func(o *parserOptions) {
		o.fragmentPercentEncodeSet = encodeSet
	})
}

// WithSkipTrailingSlashNormalization skips normalizing of empty paths.
//
// This API is EXPERIMENTAL.
func WithSkipTrailingSlashNormalization() ParserOption {
	return newFuncParserOption(func(o *parserOptions) {
		o.skipTrailingSlashNormalization = true
	})
}
