package renderer

import (
	"bufio"
	"encoding/json"
	"fmt"
	"html/template"
	"io"
	"mime"
	"net/http"
	"net/url"
	"os"
	"path"
	"path/filepath"
	"reflect"
	"strings"
	"time"

	"github.com/gocarina/gocsv"
	"github.com/gorilla/schema"
)

type Renderer interface {
	Render(http.ResponseWriter, *http.Request, interface{}, ...url.Values) error
}

type JSONRenderer struct{}
type CSVRenderer struct{}
type PDFRenderer struct{}

type htmlTemplateData struct {
	Data    interface{}
	Options url.Values
}

type JsonResponse struct {
	Result []byte
	Error  []byte
}

var (
	currRenderer Renderer
	Render       map[string]func(http.ResponseWriter, *http.Request, interface{}, ...url.Values)

	funcMap = template.FuncMap{
		"query":       query,
		"convertDate": convertDate,
	}

	contentTypeToFormat = map[string]string{
		"application/x-www-form-urlencoded": "html",
		"application/json":                  "json",
		"text/csv; charset=utf-8":           "csv",
		"application/pdf":                   "pdf",
	}
)

func init() {
	htmlRenderer, err := NewHTMLRenderer("templates/")
	if err != nil {
		panic(err)
	}

	jsonRenderer, err := NewJSONRenderer()
	if err != nil {
		panic(err)
	}

	csvRenderer, err := NewCSVRenderer()
	if err != nil {
		panic(err)
	}

	pdfRenderer, err := NewPDFRenderer()
	if err != nil {
		panic(err)
	}

	Render = make(map[string]func(http.ResponseWriter, *http.Request, interface{}, ...url.Values))

	Render["html"] = func(w http.ResponseWriter, r *http.Request, data interface{}, options ...url.Values) {
		htmlRenderer.Render(w, r, data, options...)
	}

	Render["json"] = func(w http.ResponseWriter, r *http.Request, data interface{}, options ...url.Values) {
		jsonRenderer.Render(w, r, data, options...)
	}

	Render["csv"] = func(w http.ResponseWriter, r *http.Request, data interface{}, options ...url.Values) {
		csvRenderer.Render(w, r, data, options...)
	}

	Render["pdf"] = func(w http.ResponseWriter, r *http.Request, data interface{}, options ...url.Values) {
		pdfRenderer.Render(w, r, data, options...)
	}

}

func Query(values ...string) template.URL {
	return query(values...)
}

func query(values ...string) template.URL {
	var urlValues url.Values

	urlValues = make(url.Values)
	urlValues.Set("format", "html")

	for i := 0; i < len(values); i += 2 {
		urlValues.Add(values[i], values[i+1])
	}
	return template.URL(urlValues.Encode())
}

func convertDate(t time.Time) string {
	return fmt.Sprintf("%d-%02d-%02d", t.Year(), t.Month(), t.Day())
}

// FIXME: Not safe. We should check if it responds to the error interface instead.
func isErrorType(data interface{}) bool {
	return strings.Contains(strings.ToLower(reflect.TypeOf(data).String()), "error")
}

func NewJSONRenderer() (*JSONRenderer, error) {
	return &JSONRenderer{}, nil
}

func (rend *JSONRenderer) Render(w http.ResponseWriter, r *http.Request, data interface{}, options ...url.Values) error {
	w.Header().Set("Content-Type", "application/json; charset=utf-8")
	if isErrorType(data) {
		j, err := json.Marshal(JsonResponse{nil, []byte(data.(error).Error())})
		if err != nil {
			return err
		}
		w.WriteHeader(http.StatusInternalServerError)
		w.Write(j)
	} else {
		result, err := json.Marshal(data)
		if err != nil {
			return err
		}

		response, err := json.Marshal(JsonResponse{result, nil})
		if err != nil {
			return err
		}
		w.Write(response)
	}
	return nil
}

func NewCSVRenderer() (*CSVRenderer, error) {
	return &CSVRenderer{}, nil
}

func (rend *CSVRenderer) Render(w http.ResponseWriter, r *http.Request, data interface{}, options ...url.Values) error {
	w.Header().Set("Content-Type", "text/csv; charset=utf-8")
	if isErrorType(data) {
		j, err := json.Marshal(JsonResponse{nil, []byte(data.(error).Error())})
		if err != nil {
			return err
		}
		w.WriteHeader(http.StatusInternalServerError)
		w.Write(j)
	} else {

		result, err := gocsv.MarshalString(data)
		if err != nil {
			return err
		}

		response, err := json.Marshal(JsonResponse{[]byte(result), nil})
		if err != nil {
			return err
		}
		w.Write(response)
	}
	return nil
}

func NewPDFRenderer() (*PDFRenderer, error) {
	return &PDFRenderer{}, nil
}

func (rend *PDFRenderer) Render(w http.ResponseWriter, r *http.Request, data interface{}, options ...url.Values) error {
	w.Header().Set("Content-Type", "application/pdf")
	fileMap := data.(map[string]string)
	f, err := os.Open(filepath.Join("output", fileMap["document_id"], fileMap["id"], fileMap["filename"]))
	if err != nil {
		panic(err)
	}
	reader := bufio.NewReader(f)
	io.Copy(w, reader)

	return nil
}

type HTMLRenderer struct {
	TemplatePath string

	templates map[string]*template.Template
}

func NewHTMLRenderer(templatePath string) (*HTMLRenderer, error) {
	r := &HTMLRenderer{
		TemplatePath: templatePath,
		templates:    make(map[string]*template.Template),
	}
	fns, err := filepath.Glob(filepath.Join(templatePath, "*.tpl"))
	if err != nil {
		panic(err)
	}
	lfns, err := filepath.Glob(filepath.Join(templatePath, "layout", "*.tpl"))
	if err != nil {
		panic(err)
	}
	for _, fn := range fns {
		tplName := filepath.Base(fn)
		tplName = strings.TrimSuffix(tplName, path.Ext(tplName))
		tplName = strings.TrimSuffix(tplName, path.Ext(tplName))

		files := append(lfns, fn)
		if err != nil {
			return nil, err
		}
		r.templates[tplName] = template.New(tplName)
		r.templates[tplName] = template.Must(r.templates[tplName].Funcs(funcMap).ParseFiles(files...))
	}

	return r, nil
}

func Use(r Renderer) {
	currRenderer = r
}

func (rend *HTMLRenderer) writeError(w http.ResponseWriter, r *http.Request, err error) {
	t, ok := rend.templates["error"]
	if !ok {
		panic(fmt.Errorf("Error template not found! Can't proceed, sorry."))
	}

	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	e := t.ExecuteTemplate(w, "base", err)
	if e != nil {
		panic(e)
	}
}

func (rend *HTMLRenderer) Render(w http.ResponseWriter, r *http.Request, data interface{}, options ...url.Values) {
	if data != nil && isErrorType(data) {
		rend.writeError(w, r, data.(error))
	} else {
		t, ok := rend.templates[options[0]["tpl_content"][0]]
		if !ok {
			rend.writeError(w, r, fmt.Errorf("Template %s not found", options[0]["tpl_content"][0]))
		}
		w.Header().Set("Content-Type", "text/html; charset=utf-8")
		err := t.ExecuteTemplate(w, options[0]["tpl_layout"][0], &htmlTemplateData{data, options[0]})
		if err != nil {
			rend.writeError(w, r, err)
		}
	}
}

func GetContentFormat(r *http.Request) string {
	var (
		t   string
		err error
	)

	contentType := r.Header.Get("Content-type")

	if contentType == "" {
		return "application/octet-stream"
	}

	t, _, err = mime.ParseMediaType(contentType)
	if err != nil {
		return ""
	}

	return contentTypeToFormat[t]
}

func Decode(dst interface{}, r *http.Request) error {
	switch GetContentFormat(r) {

	case "html":
		var timeConverter = func(value string) reflect.Value {
			if value == "" {
				return reflect.ValueOf(time.Time{})
			}
			if v, err := time.Parse("2006-01-02", value); err == nil {
				return reflect.ValueOf(v)
			}
			return reflect.Value{}
		}

		if err := r.ParseForm(); err != nil {
			return err
		}

		decoder := schema.NewDecoder()
		decoder.RegisterConverter(time.Time{}, timeConverter)

		if err := decoder.Decode(dst, r.PostForm); err != nil {
			return err
		}
	case "json":
		decoder := json.NewDecoder(r.Body)
		err := decoder.Decode(&dst)
		if err != nil {
			return err
		}

	}

	return nil
}
