package main

import (
	"crypto/tls"
	"fmt"
	"io/ioutil"
	"net"
	"net/url"
	"strconv"
	"strings"
	"time"
)

var TlsTimeout time.Duration = time.Duration(5) * time.Second
var redirectCount int = 0


func RetrieveGemini(u *url.URL) (string, error) {
	conf := &tls.Config{
		MinVersion:         tls.VersionTLS12,
		InsecureSkipVerify: true,
	}

	conn, err := tls.DialWithDialer(&net.Dialer{Timeout: time.Duration(settings.TimeOutSeconds) * time.Second}, "tcp", u.Host, conf)
	if err != nil {
		return "", fmt.Errorf("TLS Dial Error: %s. URL: %+v", err.Error(), u)
	}

	defer conn.Close()

	_, err = conn.Write([]byte(u.String() + "\r\n"))
	if err != nil {
		return "", err
	}

	result, err := ioutil.ReadAll(conn)
	if err != nil {
		return "", err
	}

	return string(result), nil
}

func splitHeaderBody(resp string) (string, string) {
	r := strings.SplitN(resp, "\r\n", 2)
	var header, body string
	if len(r) == 2 {
		header = r[0]
		body = r[1]
	} else if len(r) == 1 {
		header = r[0]
	}

	return strings.TrimSpace(header), strings.TrimSpace(body)
}

func splitStatusMeta(header string) (int, string) {
	h := strings.SplitN(header, " ", 2)
	if len(h) != 2 {
		h = strings.SplitN(header, "\t", 2)
	}
	if len(h) != 2 || len(h[0]) < 1 {
		return -1, "Invalid response from server; mangled header"
	}
	code, err := strconv.Atoi(h[0])
	if err != nil {
		code = 99
	}
	return code, h[1]
}

func HandleRelativeURL(relLink string, current *url.URL) string {
	rel, err := url.Parse(relLink)
	if err != nil {
		return relLink
	}
	return current.ResolveReference(rel).String()
}


func ParseGeminiBody(body string, u *url.URL) string {
	// Check mime type. It wont always be text/gemini
	// Base64 encode images and return an img tag with data src
	var out strings.Builder
	splitContent := strings.Split(body, "\n")

	inPreBlock := false
	inList := false

	for i, ln := range splitContent {
		splitContent[i] = strings.Trim(ln, "\r\n")
		isPreBlockDeclaration := strings.HasPrefix(ln, "```")
		isLinkLine := strings.HasPrefix(ln, "=>") && len([]rune(ln)) > 3
		isListItem := strings.HasPrefix(ln, "* ")

		if inList && !isListItem {
			out.WriteString("</ul>\n")
			inList = false
		}

		if inPreBlock {
			if isPreBlockDeclaration {
				inPreBlock = false
				out.WriteString("</pre>\n")
			} else {
				out.WriteString(ln)
				out.WriteRune('\n')
			}
		} else {
			ln = strings.TrimSpace(ln)
			if isPreBlockDeclaration {
				inPreBlock = !inPreBlock
				alt := ""
				if len(ln) > 3 {
					alt = strings.TrimSpace(ln[3:])
				}
				if alt == "" {
					out.WriteString("<pre>")
				} else {
					out.WriteString("<pre aria-label=\"")
					out.WriteString(alt)
					out.WriteString("\">")
				}
			} else if isLinkLine {
				var link, decorator string
				subLn := strings.Trim(ln[2:], "\r\n\t \a")
				splitPoint := strings.IndexAny(subLn, " \t")
				if splitPoint < 0 || len([]rune(subLn))-1 <= splitPoint {
					link = subLn
					decorator = subLn
				} else {
					link = strings.Trim(subLn[:splitPoint], "\t\n\r \a")
					decorator = strings.Trim(subLn[splitPoint:], "\t\n\r \a")
				}

				if strings.Index(link, "://") < 0 {
					link = HandleRelativeURL(link, u)
				}
				out.WriteString(fmt.Sprintf("<a href=\"%s\">%s</a><br>\n", link, decorator))
			} else if isListItem {
				if !inList {
					inList = true
					out.WriteString("<ul>\n")
				}
				out.WriteString("\t<li>\n\t\t")
				if len([]rune(ln)) > 2 {
					out.WriteString(ln[2:])
				}
				out.WriteString("\n\t</li>\n")
			} else if strings.HasPrefix(ln, "####") {
				out.WriteString(ln)
			} else if strings.HasPrefix(ln, "###") {
				out.WriteString("<h3>\n\t")
				if len([]rune(ln)) > 3 {
					out.WriteString(ln[3:])
				}
				out.WriteString("\n</h3>\n")
			} else if strings.HasPrefix(ln, "##") {
				out.WriteString("<h2>\n\t")
				if len([]rune(ln)) > 2 {
					out.WriteString(ln[2:])
				}
				out.WriteString("\n</h2>\n")
			} else if strings.HasPrefix(ln, "#") {
				out.WriteString("<h1>\n\t")
				if len([]rune(ln)) > 1 {
					out.WriteString(ln[1:])
				}
				out.WriteString("\n</h1>\n")
			} else if strings.HasPrefix(ln, ">") {
				out.WriteString("<blockquote>\n\t")
				if len([]rune(ln)) > 1 {
					out.WriteString(ln[1:])
				}
				out.WriteString("\n</blockquote>\n")
			} else {
				out.WriteString("<p>\n\t")
				out.WriteString(strings.TrimSpace(ln))
				out.WriteString("\n</p>\n")
			}
		}
	}
	return out.String()
}

func GetGemini(u *url.URL) (string, error) {
	resp, err := RetrieveGemini(u)
	if err != nil {
		return ConvertErrorToHTML(92, err.Error()), nil
	}

	header, body := splitHeaderBody(resp)
	status, meta := splitStatusMeta(header)
	meta = strings.ToLower(meta)
	switch status {
	case 10, 1:
		return RenderSearchForm(meta, u)
	case 20, 2:
		redirectCount = 0
		if strings.HasPrefix(meta, "text/gemini") {
			return ParseGeminiBody(body, u), nil
		} else if strings.HasPrefix(meta, "text/markdown") || strings.HasPrefix(meta, "text/x-markdown") {
			return ParseMarkdown(body), nil
		} else if strings.HasPrefix(meta, "text") {
			return strings.ReplaceAll(body, "\n", "<br>\n"), nil
		} else if strings.HasPrefix(meta, "image") {
			return MakeImage(body, meta), nil
		} else {
			appCtrl.SaveFile(body)
			return "", fmt.Errorf("File Saved")
		}
	case 30, 31, 3:
		redirectCount++
		if redirectCount < 5 {
			u, err = url.Parse(meta)
			if err != nil {
				return ConvertErrorToHTML(98, "Invalid redirect"), nil
			}
			return GetGemini(u)
		} else {
			return ConvertErrorToHTML(99, "Too many redirects"), nil
		}
	case 40, 41, 42, 43, 44, 4, 5, 50, 51, 52, 53, 59:
		redirectCount = 0
		return ConvertErrorToHTML(status, meta), nil
	default:
		redirectCount = 0
		return ConvertErrorToHTML(status, "Unknown or unsupported response status"), nil
	}

	return WTFError, nil
}

