// Copyright (C) 2015 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.

package dialer

import (
	"net"
	"net/http"
	"os"
	"strings"
	"time"

	"golang.org/x/net/proxy"

	"github.com/syncthing/syncthing/lib/logger"
	"github.com/syncthing/syncthing/lib/osutil"
)

var (
	l          = logger.DefaultLogger.NewFacility("dialer", "Dialing connections")
	dialer     = proxy.FromEnvironment()
	usingProxy = dialer != proxy.Direct
)

func init() {
	l.SetDebug("dialer", strings.Contains(os.Getenv("STTRACE"), "dialer") || os.Getenv("STTRACE") == "all")
	if usingProxy {
		http.DefaultTransport = &http.Transport{
			Dial:                Dial,
			Proxy:               http.ProxyFromEnvironment,
			TLSHandshakeTimeout: 10 * time.Second,
		}

		// Defer this, so that logging gets setup.
		go func() {
			time.Sleep(500 * time.Millisecond)
			l.Infoln("Proxy settings detected")
		}()
	}
}

// Dial tries dialing via proxy if a proxy is configured, and falls back to
// a direct connection if no proxy is defined, or connecting via proxy fails.
func Dial(network, addr string) (net.Conn, error) {
	if usingProxy {
		conn, err := dialer.Dial(network, addr)
		if err == nil {
			l.Debugf("Dialing %s address %s via proxy - success, %s -> %s", network, addr, conn.LocalAddr(), conn.RemoteAddr())
			if tcpconn, ok := conn.(*net.TCPConn); ok {
				osutil.SetTCPOptions(tcpconn)
			}
			return dialerConn{
				conn, newDialerAddr(network, addr),
			}, nil
		}
		l.Debugf("Dialing %s address %s via proxy - error %s", network, addr, err)
	}

	conn, err := proxy.Direct.Dial(network, addr)
	if err == nil {
		l.Debugf("Dialing %s address %s directly - success, %s -> %s", network, addr, conn.LocalAddr(), conn.RemoteAddr())
		if tcpconn, ok := conn.(*net.TCPConn); ok {
			osutil.SetTCPOptions(tcpconn)
		}
	} else {
		l.Debugf("Dialing %s address %s directly - error %s", network, addr, err)
	}
	return conn, err
}

type dialerConn struct {
	net.Conn
	addr net.Addr
}

func (c dialerConn) RemoteAddr() net.Addr {
	return c.addr
}

func newDialerAddr(network, addr string) net.Addr {
	netaddr, err := net.ResolveIPAddr(network, addr)
	if err == nil {
		return netaddr
	}
	return fallbackAddr{network, addr}
}

type fallbackAddr struct {
	network string
	addr    string
}

func (a fallbackAddr) Network() string {
	return a.network
}

func (a fallbackAddr) String() string {
	return a.addr
}
