package helper

import (
	"crypto/md5"
	"errors"
	"fmt"
	"image"
	"io/ioutil"
	"net/http"
	"net/url"
	"os"
	"path"
	"strconv"
	"strings"

	"gitbase.de/apairon/mark2web/config"
	"github.com/ddliu/motto"
	"github.com/disintegration/imaging"
	"github.com/flosch/pongo2"
	_ "github.com/flosch/pongo2-addons"
	_ "github.com/robertkrimen/otto/underscore"
)

func init() {
	err := pongo2.ReplaceFilter("markdown", MarkdownFilter)
	if err != nil {
		panic(err)
	}

	newFilters := map[string]pongo2.FilterFunction{
		"image_process": ImageProcessFilter,
		"relative_path": RelativePathFilter,
	}
	for name, fn := range newFilters {
		err := pongo2.RegisterFilter(name, fn)
		if err != nil {
			panic(err)
		}
	}
}

// MarkdownFilter is a pongo2 filter, which converts markdown to html
func MarkdownFilter(in *pongo2.Value, param *pongo2.Value) (*pongo2.Value, *pongo2.Error) {
	return pongo2.AsSafeValue(
			string(
				renderMarkdown(
					[]byte(in.String()),
					currentPathConfig.Markdown,
				))),
		nil
}

func parseImageParams(str string) (*config.ImagingConfig, error) {
	p := config.ImagingConfig{}
	if str == "" {
		config.Merge(&p, currentPathConfig.Imaging)
		// Filename and Format are only valid for current image
		p.Filename = ""
		p.Format = ""
		return &p, nil
	}
	for _, s := range strings.Split(str, ",") {
		e := strings.Split(s, "=")
		if len(e) < 2 {
			return nil, fmt.Errorf("invalid image parameter: %s", s)
		}
		var err error
		switch e[0] {
		case "w":
			p.Width, err = strconv.Atoi(e[1])
		case "h":
			p.Height, err = strconv.Atoi(e[1])
		case "f":
			p.Filename = e[1]
		case "p":
			p.Process = e[1]
		case "a":
			p.Anchor = e[1]
		case "q":
			p.Quality, err = strconv.Atoi(e[1])
			if p.Quality < 0 || p.Quality > 100 {
				err = errors.New("q= must be between 1 and 100")
			}
		default:
			return nil, fmt.Errorf("invalid image parameter: %s", s)
		}
		if err != nil {
			return nil, fmt.Errorf("could not convert image parameter to correct value type for '%s': %s", s, err)
		}
	}
	return &p, nil
}

func getImageFromURL(url string) (image.Image, string, error) {
	resp, err := http.Get(url)
	if err != nil {
		return nil, "", fmt.Errorf("could not get url '%s': %s", url, err)
	}

	img, format, err := image.Decode(resp.Body)
	if err != nil {
		return nil, "", fmt.Errorf("could read body from url '%s': %s", url, err)
	}

	return img, format, nil
}

// ImageProcessFilter read the image url and process parameters and saves the resized/processed image
// param: w=WITDH,h=HEIGHT
func ImageProcessFilter(in *pongo2.Value, param *pongo2.Value) (*pongo2.Value, *pongo2.Error) {
	imgSource := in.String()
	p, err := parseImageParams(param.String())
	if err != nil {
		return nil, &pongo2.Error{
			Sender:    "filter:image_resize",
			OrigError: err,
		}
	}
	if p == nil {
		return nil, &pongo2.Error{
			Sender:    "filter:image_resize",
			OrigError: errors.New("no imaging config defined"),
		}
	}

	var img image.Image
	if p.Process == "" {
		p.Process = "resize"
	}
	filePrefix := fmt.Sprintf(
		"%s_%dx%d_q%03d",
		p.Process,
		p.Width,
		p.Height,
		p.Quality,
	)
	if strings.HasPrefix(imgSource, "http://") || strings.HasPrefix(imgSource, "https://") {
		// remote file
		img, p.Format, err = getImageFromURL(imgSource)
		// build filename
		if p.Filename == "" {
			var fBase string
			if u, _ := url.Parse(imgSource); u != nil {
				fBase = strings.Split(path.Base(u.Path), ".")[0]
			}

			p.Filename = fmt.Sprintf(
				"%s_%x_%s.%s",
				filePrefix,
				md5.Sum([]byte(imgSource)),
				fBase,
				p.Format,
			)
		}
	} else {
		// local file
		imgSource = ResolveInputPath(imgSource)
		img, err = imaging.Open(imgSource, imaging.AutoOrientation(true))
		if p.Filename == "" {
			p.Filename = fmt.Sprintf(
				"%s_%s",
				filePrefix,
				path.Base(imgSource),
			)
		}
	}
	if err != nil {
		return nil, &pongo2.Error{
			Sender:    "filter:image_resize",
			OrigError: fmt.Errorf("could not open image '%s': %s", imgSource, err),
		}
	}

	imgTarget := ResolveOutputPath(p.Filename)

	if f, err := os.Stat(imgTarget); err == nil && !f.IsDir() {
		Log.Noticef("skipped processing image from %s to %s, file already exists", imgSource, imgTarget)
	} else {
		Log.Noticef("processing image from %s to %s", imgSource, imgTarget)

		switch p.Process {
		case "resize":
			img = imaging.Resize(img, p.Width, p.Height, imaging.Lanczos)
		case "fit":
			img = imaging.Fit(img, p.Width, p.Height, imaging.Lanczos)
		case "fill":
			var anchor imaging.Anchor
			switch strings.ToLower(p.Anchor) {
			case "":
				fallthrough
			case "center":
				anchor = imaging.Center
			case "topleft":
				anchor = imaging.TopLeft
			case "top":
				anchor = imaging.Top
			case "topright":
				anchor = imaging.TopRight
			case "left":
				anchor = imaging.Left
			case "right":
				anchor = imaging.Right
			case "bottomleft":
				anchor = imaging.BottomLeft
			case "bottom":
				anchor = imaging.Bottom
			case "bottomright":
				anchor = imaging.BottomRight
			default:
				return nil, &pongo2.Error{
					Sender:    "filter:image_resize",
					OrigError: fmt.Errorf("unknown anchor a=%s definition", p.Anchor),
				}
			}
			img = imaging.Fill(img, p.Width, p.Height, anchor, imaging.Lanczos)
		default:
			return nil, &pongo2.Error{
				Sender:    "filter:image_resize",
				OrigError: fmt.Errorf("invalid p parameter '%s'", p.Process),
			}
		}

		var encodeOptions = make([]imaging.EncodeOption, 0)
		if p.Quality > 0 {
			encodeOptions = append(encodeOptions, imaging.JPEGQuality(p.Quality))
		}

		err = imaging.Save(img, imgTarget, encodeOptions...)
		if err != nil {
			return nil, &pongo2.Error{
				Sender:    "filter:image_resize",
				OrigError: fmt.Errorf("could save image '%s': %s", imgTarget, err),
			}
		}
	}
	return pongo2.AsValue(ResolveNavPath(p.Filename)), nil
}

// RelativePathFilter returns the relative path to navpoint based on current nav
func RelativePathFilter(in, param *pongo2.Value) (*pongo2.Value, *pongo2.Error) {
	return pongo2.AsValue(
		ResolveNavPath(
			in.String(),
		),
	), nil
}

// RegisterFilters reads a directory and register filters from files within it
func RegisterFilters(dir string) {
	files, err := ioutil.ReadDir(dir)
	if err != nil {
		Log.Panicf("could not read from template filters dir '%s': %s", dir, err)
	}
	for _, f := range files {
		if !f.IsDir() {
			switch path.Ext(f.Name()) {
			case ".js":
				fileBase := strings.TrimSuffix(f.Name(), ".js")
				jsFile := dir + "/" + f.Name()
				Log.Debugf("trying to register filter from: %s", jsFile)
				/*
					jsStr, err := ioutil.ReadFile(jsFile)
					if err != nil {
						Log.Panicf("could not read '%s': %s", jsFile, err)
					}
				*/
				vm := motto.New()
				fn, err := vm.Run(jsFile)
				if err != nil {
					Log.Panicf("error in javascript vm for '%s': %s", jsFile, err)
				}
				if !fn.IsFunction() {
					Log.Panicf("%s does not contain a function code", jsFile)
				}

				err = pongo2.RegisterFilter(
					fileBase,
					func(in, param *pongo2.Value) (out *pongo2.Value, erro *pongo2.Error) {
						thisObj, _ := vm.Object("({})")
						if currentContext != nil {
							thisObj.Set("context", *currentContext)
						}

						if err != nil {
							Log.Panicf("could not set context as in '%s': %s", jsFile, err)
						}
						ret, err := fn.Call(thisObj.Value(), in.Interface(), param.Interface())
						if err != nil {
							Log.Panicf("error in javascript file '%s' while calling returned function: %s", jsFile, err)
						}
						retGo, err := ret.Export()
						if err != nil {
							Log.Panicf("export error for '%s': %s", jsFile, err)
						}
						return pongo2.AsValue(retGo), nil
					},
				)
				if err != nil {
					Log.Panicf("could not register filter from '%s': %s", jsFile, err)
				}

			}
		}
	}

}
