// Copyright Istio Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package kubeyaml

import (
	"bufio"
	"bytes"
	"io"
	"strings"
	"unicode"
)

const (
	yamlSeparator = "---\n"
	separator     = "---"
)

// Join the given yaml parts into a single multipart document.
func Join(parts ...[]byte) []byte {
	var b bytes.Buffer

	var lastIsNewLine bool
	for _, p := range parts {
		if len(p) == 0 {
			continue
		}
		if b.Len() != 0 {
			if !lastIsNewLine {
				_, _ = b.WriteString("\n")
			}
			b.WriteString(yamlSeparator)
		}
		_, _ = b.Write(p)
		s := string(p)
		lastIsNewLine = s[len(s)-1] == '\n'
	}

	return b.Bytes()
}

// JoinString joins the given yaml parts into a single multipart document.
func JoinString(parts ...string) string {
	var st strings.Builder

	var lastIsNewLine bool
	for _, p := range parts {
		if len(p) == 0 {
			continue
		}
		if st.Len() != 0 {
			if !lastIsNewLine {
				_, _ = st.WriteString("\n")
			}
			st.WriteString(yamlSeparator)
		}
		_, _ = st.WriteString(p)
		lastIsNewLine = p[len(p)-1] == '\n'
	}

	return st.String()
}

type Reader interface {
	Read() ([]byte, error)
}

// YAMLReader adapts from Kubernetes YAMLReader(apimachinery.k8s.io/pkg/util/yaml/decoder.go).
// It records the start line number of the chunk it reads each time.
type YAMLReader struct {
	reader   Reader
	currLine int
}

func NewYAMLReader(r *bufio.Reader) *YAMLReader {
	return &YAMLReader{
		reader:   &LineReader{reader: r},
		currLine: 0,
	}
}

// Read returns a full YAML document and its first line number.
func (r *YAMLReader) Read() ([]byte, int, error) {
	var buffer bytes.Buffer
	startLine := r.currLine + 1
	foundStart := false
	for {
		r.currLine++
		line, err := r.reader.Read()
		if err != nil && err != io.EOF {
			return nil, startLine, err
		}

		// detect beginning of the chunk
		if !bytes.Equal(line, []byte("\n")) && !bytes.Equal(line, []byte(yamlSeparator)) && !foundStart {
			startLine = r.currLine
			foundStart = true
		}

		sep := len([]byte(separator))
		if i := bytes.Index(line, []byte(separator)); i == 0 {
			// We have a potential document terminator
			i += sep
			after := line[i:]
			if len(strings.TrimRightFunc(string(after), unicode.IsSpace)) == 0 {
				if buffer.Len() != 0 {
					return buffer.Bytes(), startLine, nil
				}
				if err == io.EOF {
					return nil, startLine, err
				}
			}
		}
		if err == io.EOF {
			if buffer.Len() != 0 {
				// If we're at EOF, we have a final, non-terminated line. Return it.
				return buffer.Bytes(), startLine, nil
			}
			return nil, startLine, err
		}
		buffer.Write(line)
	}
}

type LineReader struct {
	reader *bufio.Reader
}

// Read returns a single line (with '\n' ended) from the underlying reader.
// An error is returned iff there is an error with the underlying reader.
func (r *LineReader) Read() ([]byte, error) {
	var (
		isPrefix = true
		err      error
		line     []byte
		buffer   bytes.Buffer
	)

	for isPrefix && err == nil {
		line, isPrefix, err = r.reader.ReadLine()
		buffer.Write(line)
	}
	buffer.WriteByte('\n')
	return buffer.Bytes(), err
}
