// Contains the building blocks of a gopher client: history, url, and view.
// History handles the browsing session and view represents individual
// text based resources, the url represents a parsed url.
package gopher

import (
	"errors"
	"fmt"
	"io/ioutil"
	"net"
	"strings"
	"time"
)

//------------------------------------------------\\
// + + +          V A R I A B L E S          + + + \\
//--------------------------------------------------\\

// types is a map of gophertypes to a string representing their
// type, to be used when displaying gophermaps
var types = map[string]string{
	"0": "TXT",
	"1": "MAP",
	"3": "ERR",
	"4": "BIN",
	"5": "DOS",
	"6": "UUE",
	"7": "FTS",
	"8": "TEL",
	"9": "BIN",
	"g": "GIF",
	"G": "GEM",
	"h": "HTM",
	"I": "IMG",
	"p": "PNG",
	"s": "SND",
	"S": "SSH",
	"T": "TEL",
}

var Timeout time.Duration = time.Duration(15) * time.Second

//------------------------------------------------\\
// + + +          F U N C T I O N S          + + + \\
//--------------------------------------------------\\

// Retrieve makes a request to a Url and resturns
// the response as []byte/error. This function is
// available to use directly, but in most implementations
// using the "Visit" receiver of the History struct will
// be better.
func Retrieve(host, port, resource string) ([]byte, error) {
	nullRes := make([]byte, 0)

	if host == "" || port == "" {
		return nullRes, errors.New("Incomplete request url")
	}

	addr := host + ":" + port

	conn, err := net.DialTimeout("tcp", addr, Timeout)
	if err != nil {
		return nullRes, err
	}

	send := resource + "\n"

	_, err = conn.Write([]byte(send))
	if err != nil {
		return nullRes, err
	}

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

	return result, nil
}

// Visit handles the making of the request, parsing of maps, and returning
// the correct information to the client
func Visit(gophertype, host, port, resource string) (string, []string, error) {
	resp, err := Retrieve(host, port, resource)
	if err != nil {
		return "", []string{}, err
	}

	text := string(resp)
	links := []string{}

	if IsDownloadOnly(gophertype) {
		return text, []string{}, nil
	}

	if gophertype == "1" {
		text, links = parseMap(text)
	}

	return text, links, nil
}

func getType(t string) string {
	if val, ok := types[t]; ok {
		return val
	}

	return "???"
}

func isWebLink(resource string) (string, bool) {
	split := strings.SplitN(resource, ":", 2)
	if first := strings.ToUpper(split[0]); first == "URL" && len(split) > 1 {
		return split[1], true
	}
	return "", false
}

func parseMap(text string) (string, []string) {
	splitContent := strings.Split(text, "\n")
	links := make([]string, 0, 10)

	for i, e := range splitContent {
		e = strings.Trim(e, "\r\n")
		if e == "." {
			splitContent[i] = ""
			continue
		}

		line := strings.Split(e, "\t")
		var title string

		if len(line[0]) > 1 {
			title = line[0][1:]
		} else if len(line[0]) == 1 {
			title = ""
		} else {
			title = ""
			line[0] = "i"
		}

		if len(line) < 4 || strings.HasPrefix(line[0], "i") {
			splitContent[i] = "           " + string(title)
		} else {
			link := buildLink(line[2], line[3], string(line[0][0]), line[1])
			links = append(links, link)
			linkNum := fmt.Sprintf("[%d]",len(links))
			linktext := fmt.Sprintf("%s %5s  %s", getType(string(line[0][0])), linkNum, title)
			splitContent[i] = linktext
		}
	}
	return strings.Join(splitContent, "\n"), links
}

// Returns false for all text formats (including html
// even though it may link out. Things like telnet
// should never make it into the retrieve call for
// this module, having been handled in the client
// based on their protocol.
func IsDownloadOnly(gophertype string) bool {
	switch gophertype {
	case "0", "1", "3", "7", "h":
		return false
	default:
		return true
	}
}

func buildLink(host, port, gtype, resource string) string {
	switch gtype {
	case "8", "T":
		return fmt.Sprintf("telnet://%s:%s", host, port)
	case "G":
		return fmt.Sprintf("gemini://%s:%s%s", host, port, resource)
	case "h":
		u, tf := isWebLink(resource)
		if tf {
			if strings.Index(u, "://") > 0 {
				return u
			} else {
				return fmt.Sprintf("http://%s", u)
			}
		}
		return fmt.Sprintf("gopher://%s:%s/h%s", host, port, resource)
	default:
		return fmt.Sprintf("gopher://%s:%s/%s%s", host, port, gtype, resource)
	}
}
