package formatter

import (
	"bytes"
	"fmt"
	"strings"
	"text/template"

	"github.com/docker/distribution/reference"
	"github.com/docker/docker/api/types"
	units "github.com/docker/go-units"
)

const (
	defaultDiskUsageImageTableFormat     = "table {{.Repository}}\t{{.Tag}}\t{{.ID}}\t{{.CreatedSince}} ago\t{{.VirtualSize}}\t{{.SharedSize}}\t{{.UniqueSize}}\t{{.Containers}}"
	defaultDiskUsageContainerTableFormat = "table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.LocalVolumes}}\t{{.Size}}\t{{.RunningFor}} ago\t{{.Status}}\t{{.Names}}"
	defaultDiskUsageVolumeTableFormat    = "table {{.Name}}\t{{.Links}}\t{{.Size}}"
	defaultDiskUsageTableFormat          = "table {{.Type}}\t{{.TotalCount}}\t{{.Active}}\t{{.Size}}\t{{.Reclaimable}}"

	typeHeader        = "TYPE"
	totalHeader       = "TOTAL"
	activeHeader      = "ACTIVE"
	reclaimableHeader = "RECLAIMABLE"
	containersHeader  = "CONTAINERS"
	sharedSizeHeader  = "SHARED SIZE"
	uniqueSizeHeader  = "UNIQUE SiZE"
)

// DiskUsageContext contains disk usage specific information required by the formater, encapsulate a Context struct.
type DiskUsageContext struct {
	Context
	Verbose    bool
	LayersSize int64
	Images     []*types.ImageSummary
	Containers []*types.Container
	Volumes    []*types.Volume
}

func (ctx *DiskUsageContext) startSubsection(format string) (*template.Template, error) {
	ctx.buffer = bytes.NewBufferString("")
	ctx.header = ""
	ctx.Format = Format(format)
	ctx.preFormat()

	return ctx.parseFormat()
}

func (ctx *DiskUsageContext) Write() {
	if ctx.Verbose == false {
		ctx.buffer = bytes.NewBufferString("")
		ctx.Format = defaultDiskUsageTableFormat
		ctx.preFormat()

		tmpl, err := ctx.parseFormat()
		if err != nil {
			return
		}

		err = ctx.contextFormat(tmpl, &diskUsageImagesContext{
			totalSize: ctx.LayersSize,
			images:    ctx.Images,
		})
		if err != nil {
			return
		}
		err = ctx.contextFormat(tmpl, &diskUsageContainersContext{
			containers: ctx.Containers,
		})
		if err != nil {
			return
		}

		err = ctx.contextFormat(tmpl, &diskUsageVolumesContext{
			volumes: ctx.Volumes,
		})
		if err != nil {
			return
		}

		ctx.postFormat(tmpl, &diskUsageContainersContext{containers: []*types.Container{}})

		return
	}

	// First images
	tmpl, err := ctx.startSubsection(defaultDiskUsageImageTableFormat)
	if err != nil {
		return
	}

	ctx.Output.Write([]byte("Images space usage:\n\n"))
	for _, i := range ctx.Images {
		repo := "<none>"
		tag := "<none>"
		if len(i.RepoTags) > 0 && !isDangling(*i) {
			// Only show the first tag
			ref, err := reference.ParseNamed(i.RepoTags[0])
			if err != nil {
				continue
			}
			if nt, ok := ref.(reference.NamedTagged); ok {
				repo = ref.Name()
				tag = nt.Tag()
			}
		}

		err = ctx.contextFormat(tmpl, &imageContext{
			repo:  repo,
			tag:   tag,
			trunc: true,
			i:     *i,
		})
		if err != nil {
			return
		}
	}
	ctx.postFormat(tmpl, &imageContext{})

	// Now containers
	ctx.Output.Write([]byte("\nContainers space usage:\n\n"))
	tmpl, err = ctx.startSubsection(defaultDiskUsageContainerTableFormat)
	if err != nil {
		return
	}
	for _, c := range ctx.Containers {
		// Don't display the virtual size
		c.SizeRootFs = 0
		err = ctx.contextFormat(tmpl, &containerContext{
			trunc: true,
			c:     *c,
		})
		if err != nil {
			return
		}
	}
	ctx.postFormat(tmpl, &containerContext{})

	// And volumes
	ctx.Output.Write([]byte("\nLocal Volumes space usage:\n\n"))
	tmpl, err = ctx.startSubsection(defaultDiskUsageVolumeTableFormat)
	if err != nil {
		return
	}
	for _, v := range ctx.Volumes {
		err = ctx.contextFormat(tmpl, &volumeContext{
			v: *v,
		})
		if err != nil {
			return
		}
	}
	ctx.postFormat(tmpl, &volumeContext{v: types.Volume{}})
}

type diskUsageImagesContext struct {
	HeaderContext
	totalSize int64
	images    []*types.ImageSummary
}

func (c *diskUsageImagesContext) Type() string {
	c.AddHeader(typeHeader)
	return "Images"
}

func (c *diskUsageImagesContext) TotalCount() string {
	c.AddHeader(totalHeader)
	return fmt.Sprintf("%d", len(c.images))
}

func (c *diskUsageImagesContext) Active() string {
	c.AddHeader(activeHeader)
	used := 0
	for _, i := range c.images {
		if i.Containers > 0 {
			used++
		}
	}

	return fmt.Sprintf("%d", used)
}

func (c *diskUsageImagesContext) Size() string {
	c.AddHeader(sizeHeader)
	return units.HumanSize(float64(c.totalSize))

}

func (c *diskUsageImagesContext) Reclaimable() string {
	var used int64

	c.AddHeader(reclaimableHeader)
	for _, i := range c.images {
		if i.Containers != 0 {
			if i.VirtualSize == -1 || i.SharedSize == -1 {
				continue
			}
			used += i.VirtualSize - i.SharedSize
		}
	}

	reclaimable := c.totalSize - used
	if c.totalSize > 0 {
		return fmt.Sprintf("%s (%v%%)", units.HumanSize(float64(reclaimable)), (reclaimable*100)/c.totalSize)
	}
	return fmt.Sprintf("%s", units.HumanSize(float64(reclaimable)))
}

type diskUsageContainersContext struct {
	HeaderContext
	verbose    bool
	containers []*types.Container
}

func (c *diskUsageContainersContext) Type() string {
	c.AddHeader(typeHeader)
	return "Containers"
}

func (c *diskUsageContainersContext) TotalCount() string {
	c.AddHeader(totalHeader)
	return fmt.Sprintf("%d", len(c.containers))
}

func (c *diskUsageContainersContext) isActive(container types.Container) bool {
	return strings.Contains(container.State, "running") ||
		strings.Contains(container.State, "paused") ||
		strings.Contains(container.State, "restarting")
}

func (c *diskUsageContainersContext) Active() string {
	c.AddHeader(activeHeader)
	used := 0
	for _, container := range c.containers {
		if c.isActive(*container) {
			used++
		}
	}

	return fmt.Sprintf("%d", used)
}

func (c *diskUsageContainersContext) Size() string {
	var size int64

	c.AddHeader(sizeHeader)
	for _, container := range c.containers {
		size += container.SizeRw
	}

	return units.HumanSize(float64(size))
}

func (c *diskUsageContainersContext) Reclaimable() string {
	var reclaimable int64
	var totalSize int64

	c.AddHeader(reclaimableHeader)
	for _, container := range c.containers {
		if !c.isActive(*container) {
			reclaimable += container.SizeRw
		}
		totalSize += container.SizeRw
	}

	if totalSize > 0 {
		return fmt.Sprintf("%s (%v%%)", units.HumanSize(float64(reclaimable)), (reclaimable*100)/totalSize)
	}

	return fmt.Sprintf("%s", units.HumanSize(float64(reclaimable)))
}

type diskUsageVolumesContext struct {
	HeaderContext
	verbose bool
	volumes []*types.Volume
}

func (c *diskUsageVolumesContext) Type() string {
	c.AddHeader(typeHeader)
	return "Local Volumes"
}

func (c *diskUsageVolumesContext) TotalCount() string {
	c.AddHeader(totalHeader)
	return fmt.Sprintf("%d", len(c.volumes))
}

func (c *diskUsageVolumesContext) Active() string {
	c.AddHeader(activeHeader)

	used := 0
	for _, v := range c.volumes {
		if v.UsageData.RefCount > 0 {
			used++
		}
	}

	return fmt.Sprintf("%d", used)
}

func (c *diskUsageVolumesContext) Size() string {
	var size int64

	c.AddHeader(sizeHeader)
	for _, v := range c.volumes {
		if v.UsageData.Size != -1 {
			size += v.UsageData.Size
		}
	}

	return units.HumanSize(float64(size))
}

func (c *diskUsageVolumesContext) Reclaimable() string {
	var reclaimable int64
	var totalSize int64

	c.AddHeader(reclaimableHeader)
	for _, v := range c.volumes {
		if v.UsageData.Size != -1 {
			if v.UsageData.RefCount == 0 {
				reclaimable += v.UsageData.Size
			}
			totalSize += v.UsageData.Size
		}
	}

	if totalSize > 0 {
		return fmt.Sprintf("%s (%v%%)", units.HumanSize(float64(reclaimable)), (reclaimable*100)/totalSize)
	}

	return fmt.Sprintf("%s", units.HumanSize(float64(reclaimable)))
}
