/*
 * Copyright 2022 CloudWeGo 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 generator

import (
	"bytes"
	"fmt"
	"io/ioutil"
	"path/filepath"
	"strings"
	"text/template"

	"github.com/cloudwego/hertz/cmd/hz/util"
	"github.com/cloudwego/hertz/cmd/hz/util/logs"
)

type FilePathRenderInfo struct {
	MasterIDLName  string // master IDL name
	GenPackage     string // master IDL generate code package
	HandlerDir     string // handler generate dir
	ModelDir       string // model generate dir
	RouterDir      string // router generate dir
	ProjectDir     string // projectDir
	GoModule       string // go module
	ServiceName    string // service name, changed as services are traversed
	MethodName     string // method name, changed as methods are traversed
	HandlerGenPath string // "api.gen_path" value
}

type IDLPackageRenderInfo struct {
	FilePathRenderInfo
	ServiceInfos *HttpPackage
}

type CustomizedFileForMethod struct {
	*HttpMethod
	FilePath       string
	FilePackage    string
	ServiceInfo    *Service              // service info for this method
	IDLPackageInfo *IDLPackageRenderInfo // IDL info for this service
}

type CustomizedFileForService struct {
	*Service
	FilePath       string
	FilePackage    string
	IDLPackageInfo *IDLPackageRenderInfo // IDL info for this service
}

type CustomizedFileForIDL struct {
	*IDLPackageRenderInfo
	FilePath    string
	FilePackage string
}

// todo: 1. how to import other file, if the other file name is a template

// genCustomizedFile generate customized file template
func (pkgGen *HttpPackageGenerator) genCustomizedFile(pkg *HttpPackage) error {
	filePathRenderInfo := FilePathRenderInfo{
		MasterIDLName: pkg.IdlName,
		GenPackage:    pkg.Package,
		HandlerDir:    pkgGen.HandlerDir,
		ModelDir:      pkgGen.ModelDir,
		RouterDir:     pkgGen.RouterDir,
		ProjectDir:    pkgGen.OutputDir,
		GoModule:      pkgGen.ProjPackage,
		// methodName & serviceName will change as traverse
	}

	idlPackageRenderInfo := IDLPackageRenderInfo{
		FilePathRenderInfo: filePathRenderInfo,
		ServiceInfos:       pkg,
	}

	for _, tplInfo := range pkgGen.tplsInfo {
		// the default template has been automatically generated by the tool, so skip
		if tplInfo.Default {
			continue
		}

		// loop generate file
		if tplInfo.LoopService || tplInfo.LoopMethod {
			loopMethod := tplInfo.LoopMethod
			loopService := tplInfo.LoopService
			if loopService && !loopMethod { // only loop service
				for _, service := range idlPackageRenderInfo.ServiceInfos.Services {
					filePathRenderInfo.ServiceName = service.Name
					err := pkgGen.genLoopService(tplInfo, filePathRenderInfo, service, &idlPackageRenderInfo)
					if err != nil {
						return err
					}
				}
			} else { // loop service & method, because if loop method, the service must be looped
				for _, service := range idlPackageRenderInfo.ServiceInfos.Services {
					for _, method := range service.Methods {
						filePathRenderInfo.ServiceName = service.Name
						filePathRenderInfo.MethodName = method.Name
						filePathRenderInfo.HandlerGenPath = method.OutputDir
						err := pkgGen.genLoopMethod(tplInfo, filePathRenderInfo, method, service, &idlPackageRenderInfo)
						if err != nil {
							return err
						}
					}
				}
			}
		} else { // generate customized file single
			err := pkgGen.genSingleCustomizedFile(tplInfo, filePathRenderInfo, idlPackageRenderInfo)
			if err != nil {
				return err
			}
		}
	}
	return nil
}

// renderFilePath used to render file path template to get real file path
func renderFilePath(tplInfo *Template, filePathRenderInfo FilePathRenderInfo) (string, error) {
	tpl, err := template.New(tplInfo.Path).Funcs(funcMap).Parse(tplInfo.Path)
	if err != nil {
		return "", fmt.Errorf("parse file path template(%s) failed, err: %v", tplInfo.Path, err)
	}
	filePath := bytes.NewBuffer(nil)
	err = tpl.Execute(filePath, filePathRenderInfo)
	if err != nil {
		return "", fmt.Errorf("render file path template(%s) failed, err: %v", tplInfo.Path, err)
	}

	return filePath.String(), nil
}

func renderInsertKey(tplInfo *Template, data interface{}) (string, error) {
	tpl, err := template.New(tplInfo.UpdateBehavior.InsertKey).Funcs(funcMap).Parse(tplInfo.UpdateBehavior.InsertKey)
	if err != nil {
		return "", fmt.Errorf("parse insert key template(%s) failed, err: %v", tplInfo.UpdateBehavior.InsertKey, err)
	}
	insertKey := bytes.NewBuffer(nil)
	err = tpl.Execute(insertKey, data)
	if err != nil {
		return "", fmt.Errorf("render insert key template(%s) failed, err: %v", tplInfo.UpdateBehavior.InsertKey, err)
	}

	return insertKey.String(), nil
}

// renderImportTpl will render import template
// it will return the []string, like blow:
// ["import", alias "import", import]
// other format will be error
func renderImportTpl(tplInfo *Template, data interface{}) ([]string, error) {
	var importList []string
	for _, impt := range tplInfo.UpdateBehavior.ImportTpl {
		tpl, err := template.New(impt).Funcs(funcMap).Parse(impt)
		if err != nil {
			return nil, fmt.Errorf("parse import template(%s) failed, err: %v", impt, err)
		}
		imptContent := bytes.NewBuffer(nil)
		err = tpl.Execute(imptContent, data)
		if err != nil {
			return nil, fmt.Errorf("render import template(%s) failed, err: %v", impt, err)
		}
		importList = append(importList, imptContent.String())
	}
	var ret []string
	for _, impts := range importList {
		// 'import render result' may have multiple imports
		if strings.Contains(impts, "\n") {
			for _, impt := range strings.Split(impts, "\n") {
				ret = append(ret, impt)
			}
		} else {
			ret = append(ret, impts)
		}
	}

	return ret, nil
}

// renderAppendContent used to render append content for 'update' command
func renderAppendContent(tplInfo *Template, renderInfo interface{}) (string, error) {
	tpl, err := template.New(tplInfo.Path).Funcs(funcMap).Parse(tplInfo.UpdateBehavior.AppendTpl)
	if err != nil {
		return "", fmt.Errorf("parse append content template(%s) failed, err: %v", tplInfo.Path, err)
	}
	appendContent := bytes.NewBuffer(nil)
	err = tpl.Execute(appendContent, renderInfo)
	if err != nil {
		return "", fmt.Errorf("render append content template(%s) failed, err: %v", tplInfo.Path, err)
	}

	return appendContent.String(), nil
}

// appendUpdateFile used to append content to file for 'update' command
func appendUpdateFile(tplInfo *Template, renderInfo interface{}, fileContent []byte) ([]byte, error) {
	// render insert content
	appendContent, err := renderAppendContent(tplInfo, renderInfo)
	if err != nil {
		return []byte(""), err
	}
	buf := bytes.NewBuffer(nil)
	_, err = buf.Write(fileContent)
	if err != nil {
		return []byte(""), fmt.Errorf("write file(%s) failed, err: %v", tplInfo.Path, err)
	}
	// "\r\n" && "\n" has the same suffix
	if !bytes.HasSuffix(buf.Bytes(), []byte("\n")) {
		_, err = buf.WriteString("\n")
		if err != nil {
			return []byte(""), fmt.Errorf("write file(%s) line break failed, err: %v", tplInfo.Path, err)
		}
	}
	_, err = buf.WriteString(appendContent)
	if err != nil {
		return []byte(""), fmt.Errorf("append file(%s) failed, err: %v", tplInfo.Path, err)
	}

	return buf.Bytes(), nil
}

func getInsertImportContent(tplInfo *Template, renderInfo interface{}, fileContent []byte) ([][2]string, error) {
	importContent, err := renderImportTpl(tplInfo, renderInfo)
	if err != nil {
		return nil, err
	}
	var imptSlice [][2]string
	for _, impt := range importContent {
		// import has to format
		// 1. alias "import"
		// 2. "import"
		// 3. import (can not contain '"')
		impt = strings.TrimSpace(impt)
		if !strings.Contains(impt, "\"") { // 3. import (can not contain '"')
			if bytes.Contains(fileContent, []byte(impt)) {
				continue
			}
			imptSlice = append(imptSlice, [2]string{"", impt})
		} else {
			if !strings.HasSuffix(impt, "\"") {
				return nil, fmt.Errorf("import can not has suffix \"\"\", for file: %s", tplInfo.Path)
			}
			if strings.HasPrefix(impt, "\"") { // 2. "import"
				if bytes.Contains(fileContent, []byte(impt[1:len(impt)-1])) {
					continue
				}
				imptSlice = append(imptSlice, [2]string{"", impt[1 : len(impt)-1]})
			} else { // 3. alias "import"
				idx := strings.Index(impt, "\n")
				if idx == -1 {
					return nil, fmt.Errorf("error import format for file: %s", tplInfo.Path)
				}
				if bytes.Contains(fileContent, []byte(impt[idx+1:len(impt)-1])) {
					continue
				}
				imptSlice = append(imptSlice, [2]string{impt[:idx], impt[idx+1 : len(impt)-1]})
			}
		}
	}

	return imptSlice, nil
}

// genLoopService used to generate files by 'service'
func (pkgGen *HttpPackageGenerator) genLoopService(tplInfo *Template, filePathRenderInfo FilePathRenderInfo, service *Service, idlPackageRenderInfo *IDLPackageRenderInfo) error {
	filePath, err := renderFilePath(tplInfo, filePathRenderInfo)
	if err != nil {
		return err
	}
	// determine if a custom file exists
	exist, err := util.PathExist(filePath)
	if err != nil {
		return fmt.Errorf("judge file(%s) exists failed, err: %v", filePath, err)
	}
	if !exist { // create file
		data := CustomizedFileForService{
			Service:        service,
			FilePath:       filePath,
			FilePackage:    util.SplitPackage(filepath.Dir(filePath), ""),
			IDLPackageInfo: idlPackageRenderInfo,
		}
		err = pkgGen.TemplateGenerator.Generate(data, tplInfo.Path, filePath, false)
		if err != nil {
			return err
		}
	} else {
		switch tplInfo.UpdateBehavior.Type {
		case Skip:
			// do nothing
			logs.Infof("do not update file '%s', because the update behavior is 'Unchanged'", filePath)
		case Cover:
			// re-generate
			logs.Infof("re-generate file '%s', because the update behavior is 'Regenerate'", filePath)
			data := CustomizedFileForService{
				Service:        service,
				FilePath:       filePath,
				FilePackage:    util.SplitPackage(filepath.Dir(filePath), ""),
				IDLPackageInfo: idlPackageRenderInfo,
			}
			err := pkgGen.TemplateGenerator.Generate(data, tplInfo.Path, filePath, false)
			if err != nil {
				return err
			}
		case Append: // todo: append logic need to be optimized for method
			fileContent, err := ioutil.ReadFile(filePath)
			if err != nil {
				return err
			}
			var appendContent []byte
			// get insert content
			if tplInfo.UpdateBehavior.AppendKey == "method" {
				for _, method := range service.Methods {
					data := CustomizedFileForMethod{
						HttpMethod:     method,
						FilePath:       filePath,
						FilePackage:    util.SplitPackage(filepath.Dir(filePath), ""),
						ServiceInfo:    service,
						IDLPackageInfo: idlPackageRenderInfo,
					}
					insertKey, err := renderInsertKey(tplInfo, data)
					if err != nil {
						return err
					}
					if bytes.Contains(fileContent, []byte(insertKey)) {
						continue
					}
					imptSlice, err := getInsertImportContent(tplInfo, data, fileContent)
					if err != nil {
						return err
					}
					// insert new import to the fileContent
					for _, impt := range imptSlice {
						if bytes.Contains(fileContent, []byte(impt[1])) {
							continue
						}
						fileContent, err = util.AddImportForContent(fileContent, impt[0], impt[1])
						// insert import error do not influence the generated file
						if err != nil {
							logs.Warnf("can not add import(%s) for file(%s), err: %v\n", impt[1], filePath, err)
						}
					}
					appendContent, err = appendUpdateFile(tplInfo, data, appendContent)
					if err != nil {
						return err
					}
				}
				if len(tplInfo.UpdateBehavior.AppendLocation) == 0 { // default, append to end of file
					buf := bytes.NewBuffer(nil)
					_, err = buf.Write(fileContent)
					if err != nil {
						return fmt.Errorf("write file(%s) failed, err: %v", tplInfo.Path, err)
					}
					_, err = buf.Write(appendContent)
					if err != nil {
						return fmt.Errorf("append file(%s) failed, err: %v", tplInfo.Path, err)
					}
					logs.Infof("append content for file '%s', because the update behavior is 'Append' and appendKey is 'method'", filePath)
					pkgGen.files = append(pkgGen.files, File{filePath, buf.String(), false, ""})
				} else { // 'append location', append new content after 'append location'
					part := bytes.Split(fileContent, []byte(tplInfo.UpdateBehavior.AppendLocation))
					if len(part) == 0 {
						return fmt.Errorf("can not find append location '%s' for file '%s'\n", tplInfo.UpdateBehavior.AppendLocation, filePath)
					}
					if len(part) != 2 {
						return fmt.Errorf("do not support multiple append location '%s' for file '%s'\n", tplInfo.UpdateBehavior.AppendLocation, filePath)
					}
					buf := bytes.NewBuffer(nil)
					err = writeBytes(buf, part[0], []byte(tplInfo.UpdateBehavior.AppendLocation), appendContent, part[1])
					if err != nil {
						return fmt.Errorf("write file(%s) failed, err: %v", tplInfo.Path, err)
					}
					logs.Infof("append content for file '%s', because the update behavior is 'Append' and appendKey is 'method'", filePath)
					pkgGen.files = append(pkgGen.files, File{filePath, buf.String(), false, ""})
				}
			} else {
				logs.Warnf("Loop 'service' field for '%s' only append content by appendKey for 'method', so cannot append content", filePath)
			}
		default:
			// do nothing
			logs.Warnf("unknown update behavior, do nothing")
		}
	}
	return nil
}

// genLoopMethod used to generate files by 'method'
func (pkgGen *HttpPackageGenerator) genLoopMethod(tplInfo *Template, filePathRenderInfo FilePathRenderInfo, method *HttpMethod, service *Service, idlPackageRenderInfo *IDLPackageRenderInfo) error {
	filePath, err := renderFilePath(tplInfo, filePathRenderInfo)
	if err != nil {
		return err
	}
	// determine if a custom file exists
	exist, err := util.PathExist(filePath)
	if err != nil {
		return fmt.Errorf("judge file(%s) exists failed, err: %v", filePath, err)
	}

	if !exist { // create file
		data := CustomizedFileForMethod{
			HttpMethod:     method,
			FilePath:       filePath,
			FilePackage:    util.SplitPackage(filepath.Dir(filePath), ""),
			ServiceInfo:    service,
			IDLPackageInfo: idlPackageRenderInfo,
		}
		err := pkgGen.TemplateGenerator.Generate(data, tplInfo.Path, filePath, false)
		if err != nil {
			return err
		}
	} else {
		switch tplInfo.UpdateBehavior.Type {
		case Skip:
			// do nothing
			logs.Infof("do not update file '%s', because the update behavior is 'Unchanged'", filePath)
		case Cover:
			// re-generate
			logs.Infof("re-generate file '%s', because the update behavior is 'Regenerate'", filePath)
			data := CustomizedFileForMethod{
				HttpMethod:     method,
				FilePath:       filePath,
				FilePackage:    util.SplitPackage(filepath.Dir(filePath), ""),
				ServiceInfo:    service,
				IDLPackageInfo: idlPackageRenderInfo,
			}
			err := pkgGen.TemplateGenerator.Generate(data, tplInfo.Path, filePath, false)
			if err != nil {
				return err
			}
		case Append:
			// for loop method, no need to append something; so do nothing
			logs.Warnf("do not append content for file '%s', because the update behavior is 'Append' and loop 'method' have no need to append content", filePath)
		default:
			// do nothing
			logs.Warnf("unknown update behavior, do nothing")
		}
	}

	return nil
}

// genSingleCustomizedFile used to generate file by 'master IDL'
func (pkgGen *HttpPackageGenerator) genSingleCustomizedFile(tplInfo *Template, filePathRenderInfo FilePathRenderInfo, idlPackageRenderInfo IDLPackageRenderInfo) error {
	// generate file single
	filePath, err := renderFilePath(tplInfo, filePathRenderInfo)
	if err != nil {
		return err
	}
	// determine if a custom file exists
	exist, err := util.PathExist(filePath)
	if err != nil {
		return fmt.Errorf("judge file(%s) exists failed, err: %v", filePath, err)
	}

	if !exist { // create file
		data := CustomizedFileForIDL{
			IDLPackageRenderInfo: &idlPackageRenderInfo,
			FilePath:             filePath,
			FilePackage:          util.SplitPackage(filepath.Dir(filePath), ""),
		}
		err := pkgGen.TemplateGenerator.Generate(data, tplInfo.Path, filePath, false)
		if err != nil {
			return err
		}
	} else {
		switch tplInfo.UpdateBehavior.Type {
		case Skip:
			// do nothing
			logs.Infof("do not update file '%s', because the update behavior is 'Unchanged'", filePath)
		case Cover:
			// re-generate
			logs.Infof("re-generate file '%s', because the update behavior is 'Regenerate'", filePath)
			data := CustomizedFileForIDL{
				IDLPackageRenderInfo: &idlPackageRenderInfo,
				FilePath:             filePath,
				FilePackage:          util.SplitPackage(filepath.Dir(filePath), ""),
			}
			err := pkgGen.TemplateGenerator.Generate(data, tplInfo.Path, filePath, false)
			if err != nil {
				return err
			}
		case Append: // todo: append logic need to be optimized for single file
			fileContent, err := ioutil.ReadFile(filePath)
			if err != nil {
				return err
			}
			if tplInfo.UpdateBehavior.AppendKey == "method" {
				var appendContent []byte
				for _, service := range idlPackageRenderInfo.ServiceInfos.Services {
					for _, method := range service.Methods {
						data := CustomizedFileForMethod{
							HttpMethod:     method,
							FilePath:       filePath,
							FilePackage:    util.SplitPackage(filepath.Dir(filePath), ""),
							ServiceInfo:    service,
							IDLPackageInfo: &idlPackageRenderInfo,
						}
						insertKey, err := renderInsertKey(tplInfo, data)
						if err != nil {
							return err
						}
						if bytes.Contains(fileContent, []byte(insertKey)) {
							continue
						}
						imptSlice, err := getInsertImportContent(tplInfo, data, fileContent)
						if err != nil {
							return err
						}
						for _, impt := range imptSlice {
							if bytes.Contains(fileContent, []byte(impt[1])) {
								continue
							}
							fileContent, err = util.AddImportForContent(fileContent, impt[0], impt[1])
							if err != nil {
								logs.Warnf("can not add import(%s) for file(%s)\n", impt[1], filePath)
							}
						}

						appendContent, err = appendUpdateFile(tplInfo, data, appendContent)
						if err != nil {
							return err
						}
					}
				}
				if len(tplInfo.UpdateBehavior.AppendLocation) == 0 { // default, append to end of file
					buf := bytes.NewBuffer(nil)
					_, err = buf.Write(fileContent)
					if err != nil {
						return fmt.Errorf("write file(%s) failed, err: %v", tplInfo.Path, err)
					}
					_, err = buf.Write(appendContent)
					if err != nil {
						return fmt.Errorf("append file(%s) failed, err: %v", tplInfo.Path, err)
					}
					logs.Infof("append content for file '%s', because the update behavior is 'Append' and appendKey is 'method'", filePath)
					pkgGen.files = append(pkgGen.files, File{filePath, buf.String(), false, ""})
				} else { // 'append location', append new content after 'append location'
					part := bytes.Split(fileContent, []byte(tplInfo.UpdateBehavior.AppendLocation))
					if len(part) == 0 {
						return fmt.Errorf("can not find append location '%s' for file '%s'\n", tplInfo.UpdateBehavior.AppendLocation, filePath)
					}
					if len(part) != 2 {
						return fmt.Errorf("do not support multiple append location '%s' for file '%s'\n", tplInfo.UpdateBehavior.AppendLocation, filePath)
					}
					buf := bytes.NewBuffer(nil)
					err = writeBytes(buf, part[0], []byte(tplInfo.UpdateBehavior.AppendLocation), appendContent, part[1])
					if err != nil {
						return fmt.Errorf("write file(%s) failed, err: %v", tplInfo.Path, err)
					}
					logs.Infof("append content for file '%s', because the update behavior is 'Append' and appendKey is 'method'", filePath)
					pkgGen.files = append(pkgGen.files, File{filePath, buf.String(), false, ""})
				}
			} else if tplInfo.UpdateBehavior.AppendKey == "service" {
				var appendContent []byte
				for _, service := range idlPackageRenderInfo.ServiceInfos.Services {
					data := CustomizedFileForService{
						Service:        service,
						FilePath:       filePath,
						FilePackage:    util.SplitPackage(filepath.Dir(filePath), ""),
						IDLPackageInfo: &idlPackageRenderInfo,
					}
					insertKey, err := renderInsertKey(tplInfo, data)
					if err != nil {
						return err
					}
					if bytes.Contains(fileContent, []byte(insertKey)) {
						continue
					}
					imptSlice, err := getInsertImportContent(tplInfo, data, fileContent)
					if err != nil {
						return err
					}
					for _, impt := range imptSlice {
						if bytes.Contains(fileContent, []byte(impt[1])) {
							continue
						}
						fileContent, err = util.AddImportForContent(fileContent, impt[0], impt[1])
						if err != nil {
							logs.Warnf("can not add import(%s) for file(%s)\n", impt[1], filePath)
						}
					}
					appendContent, err = appendUpdateFile(tplInfo, data, appendContent)
					if err != nil {
						return err
					}
				}
				if len(tplInfo.UpdateBehavior.AppendLocation) == 0 { // default, append to end of file
					buf := bytes.NewBuffer(nil)
					_, err = buf.Write(fileContent)
					if err != nil {
						return fmt.Errorf("write file(%s) failed, err: %v", tplInfo.Path, err)
					}
					_, err = buf.Write(appendContent)
					if err != nil {
						return fmt.Errorf("append file(%s) failed, err: %v", tplInfo.Path, err)
					}
					logs.Infof("append content for file '%s', because the update behavior is 'Append' and appendKey is 'service'", filePath)
					pkgGen.files = append(pkgGen.files, File{filePath, buf.String(), false, ""})
				} else { // 'append location', append new content after 'append location'
					part := bytes.Split(fileContent, []byte(tplInfo.UpdateBehavior.AppendLocation))
					if len(part) == 0 {
						return fmt.Errorf("can not find append location '%s' for file '%s'\n", tplInfo.UpdateBehavior.AppendLocation, filePath)
					}
					if len(part) != 2 {
						return fmt.Errorf("do not support multiple append location '%s' for file '%s'\n", tplInfo.UpdateBehavior.AppendLocation, filePath)
					}
					buf := bytes.NewBuffer(nil)
					err = writeBytes(buf, part[0], []byte(tplInfo.UpdateBehavior.AppendLocation), appendContent, part[1])
					if err != nil {
						return fmt.Errorf("write file(%s) failed, err: %v", tplInfo.Path, err)
					}
					logs.Infof("append content for file '%s', because the update behavior is 'Append' and appendKey is 'service'", filePath)
					pkgGen.files = append(pkgGen.files, File{filePath, buf.String(), false, ""})
				}
			} else { // add append content to the file directly
				data := CustomizedFileForIDL{
					IDLPackageRenderInfo: &idlPackageRenderInfo,
					FilePath:             filePath,
					FilePackage:          util.SplitPackage(filepath.Dir(filePath), ""),
				}
				file, err := appendUpdateFile(tplInfo, data, fileContent)
				if err != nil {
					return err
				}
				pkgGen.files = append(pkgGen.files, File{filePath, string(file), false, ""})
			}
		default:
			// do nothing
			logs.Warnf("unknown update behavior, do nothing")
		}
	}

	return nil
}

func writeBytes(buf *bytes.Buffer, bytes ...[]byte) error {
	for _, b := range bytes {
		_, err := buf.Write(b)
		if err != nil {
			return err
		}
	}

	return nil
}
