<commit_before>/*
 * Copyright (c) 2015 Joern Rischmueller (joern.rm@gmail.com)
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Affero General Public License as
 *  published by the Free Software Foundation, either version 3 of the
 *  License, or (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Affero General Public License for more details.
 *
 *  You should have received a copy of the GNU Affero General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package goldencheetah

import (
	"net/http"
	"time"
	"strconv"
	"fmt"

    "golang.org/x/net/context"
	"google.golang.org/appengine"
	"google.golang.org/appengine/datastore"

	b64 "encoding/base64"

	"github.com/emicklei/go-restful"

)

// ---------------------------------------------------------------------------------------------------------------//
// Full Golden Cheetah chart definition (chartentity) which is stored in DB
// ---------------------------------------------------------------------------------------------------------------//
type ChartEntity struct {
	Header          ChartEntityHeader
	ChartXML	    string       `datastore:",noindex"`
	Image			[]byte       `datastore:",noindex"`
	CreatorNick		string       `datastore:",noindex"`
	CreatorEmail	string       `datastore:",noindex"`
}

type ChartEntityHeaderOnly struct {
	Header          ChartEntityHeader
}

type ChartEntityHeader struct {
	Name 	   		string       `datastore:",noindex"`
	Description 	string       `datastore:",noindex"`
	Language		string       `datastore:",noindex"`
	GcVersion 		string
	LastChanged 	time.Time
	CreatorId		string
	Curated			bool
	Deleted         bool
}

// ---------------------------------------------------------------------------------------------------------------//
// API View Definition
// ---------------------------------------------------------------------------------------------------------------//

// Full structure for GET and PUT
type ChartAPIv1 struct {
	Header          ChartAPIHeaderV1 `json:"header"`
	ChartXML        string      `json:"chartxml"`
	Image           string      `json:"image"`
	CreatorNick     string      `json:"creatorNick"`
	CreatorEmail    string      `json:"creatorEmail"`

}

type ChartAPIv1List []ChartAPIv1

// Header only structure
type ChartAPIv1HeaderOnly struct {
	Header          ChartAPIHeaderV1 `json:"header"`
}
type ChartAPIv1HeaderOnlyList []ChartAPIv1HeaderOnly

// Internal Structure for Header
type ChartAPIHeaderV1 struct {
	Id				int64		`json:"id"`
	Name            string   	`json:"name"`
	Description     string      `json:"description"`
	GcVersion       string      `json:"gcversion"`
	LastChanged     string      `json:"lastChange"`
	CreatorId       string      `json:"creatorId"`
	Language        string      `json:"language"`
	Curated			bool		`json:"curated"`
	Deleted			bool		`json:"deleted"`
}

// ---------------------------------------------------------------------------------------------------------------//
// Data Storage View
// ---------------------------------------------------------------------------------------------------------------//

const chartDBEntity = "chartentity"
const chartDBEntityRootKey = "chartsroot"

func mapAPItoDBChart(api *ChartAPIv1, db *ChartEntity) {
	mapAPItoDBChartHeader(&api.Header, &db.Header)
	db.ChartXML = api.ChartXML
	data, err := b64.StdEncoding.DecodeString(api.Image)
	if err != nil{
		data = nil
	} else {
		db.Image = data
	}
	db.CreatorNick = api.CreatorNick
	db.CreatorEmail = api.CreatorEmail
}

func mapAPItoDBChartHeader(api *ChartAPIHeaderV1, db *ChartEntityHeader) {
	db.Name = api.Name
	db.Description = api.Description
	db.Language = api.Language
	db.GcVersion = api.GcVersion
	db.LastChanged, _ = time.Parse(dateTimeLayout, api.LastChanged)
	db.CreatorId = api.CreatorId
	db.Curated = api.Curated
	db.Deleted = api.Deleted
}

func mapDBtoAPIChart(db *ChartEntity, api *ChartAPIv1 ) {
	mapDBtoAPIChartHeader(&db.Header, &api.Header)
	api.ChartXML = db.ChartXML
	api.Image = b64.StdEncoding.EncodeToString(db.Image)
	api.CreatorNick = db.CreatorNick
	api.CreatorEmail = db.CreatorEmail
}

func mapDBtoAPIChartHeader(db *ChartEntityHeader, api *ChartAPIHeaderV1 ) {
	api.Name = db.Name
	api.Description = db.Description
	api.GcVersion = db.GcVersion
	api.Language = db.Language
	api.LastChanged = db.LastChanged.Format(dateTimeLayout)
	api.CreatorId = db.CreatorId
	api.Curated = db.Curated
	api.Deleted = db.Deleted
}

// supporting functions

// chartEntityKey returns the key used for all chartEntity entries.
func chartEntityRootKey(c context.Context) *datastore.Key {
	return datastore.NewKey(c, chartDBEntity, chartDBEntityRootKey, 0, nil)
}

// ---------------------------------------------------------------------------------------------------------------//
// request/response handler
// ---------------------------------------------------------------------------------------------------------------//

func insertChart(request *restful.Request, response *restful.Response) {
	c := appengine.NewContext(request.Request)

	chart := new(ChartAPIv1)
	if err := request.ReadEntity(chart); err != nil {
		addPlainTextError(response,http.StatusInternalServerError, err.Error())
		return
	}

	// No checks if the necessary fields are filed or not - since GoldenCheetah is
	// the only consumer of the APIs - any checks/response are to support this use-case

	chartDB := new(ChartEntity)
	mapAPItoDBChart(chart, chartDB)

	// complete/set POST fields
	chartDB.Header.LastChanged = time.Now()
	chartDB.Header.Curated = false
	chartDB.Header.Deleted = false

	// auto-curate if a registered "curator" is adding a chart
	curatorQuery := datastore.NewQuery(curatorDBEntity).Filter("CuratorId =", chartDB.Header.CreatorId).KeysOnly()
	var curatorOnDBList []CuratorEntity
	_, cErr := curatorQuery.GetAll(c, &curatorOnDBList)
	if cErr != nil {
		chartDB.Header.Curated = false
	} else {
		chartDB.Header.Curated = true
	}

	// and now store it
	key := datastore.NewIncompleteKey(c, chartDBEntity, chartEntityRootKey(c))
	key, err := datastore.Put(c, key, chartDB);
	if  err != nil {
		if appengine.IsOverQuota(err) {
			// return 503 and a text similar to what GAE is returning as well
			addPlainTextError(response, http.StatusServiceUnavailable, "503 - Over Quota")
		} else {
			addPlainTextError(response, http.StatusInternalServerError, err.Error())
		}
		return
	}

	// send back the key
	response.WriteHeaderAndEntity(http.StatusCreated, strconv.FormatInt(key.IntID(), 10))

}

func updateChart(request *restful.Request, response *restful.Response) {
	c := appengine.NewContext(request.Request)

	chart := new(ChartAPIv1)
	if 	err := request.ReadEntity(chart); err != nil {
		addPlainTextError(response, http.StatusInternalServerError, err.Error())
		return
	}

	if (chart.Header.Id == 0) {
		addPlainTextError(response, http.StatusBadRequest, "Mandatory Id/Key for Update is missing or invalid")
		return
	}

	// No more checks if the necessary fields are filed or not - since GoldenCheetah is
	// the only consumer of the APIs - any checks/response are to support this use-case

	chartDB := new(ChartEntity)
	mapAPItoDBChart(chart, chartDB)

	chartDB.Header.LastChanged = time.Now()

	// and now store it

	key := datastore.NewKey(c, chartDBEntity, "", chart.Header.Id, chartEntityRootKey(c))
	if 	_, err := datastore.Put(c, key, chartDB); err != nil {
		if appengine.IsOverQuota(err) {
			// return 503 and a text similar to what GAE is returning as well
			addPlainTextError(response, http.StatusServiceUnavailable, "503 - Over Quota")
		} else {
			addPlainTextError(response, http.StatusInternalServerError, err.Error())
		}
		return
	}

	// send back the key
	response.WriteHeaderAndEntity(http.StatusNoContent, strconv.FormatInt(key.IntID(), 10))

}
func getChartHeader(request *restful.Request, response *restful.Response) {
	c := appengine.NewContext(request.Request)

	var date time.Time
	var err  error
	if dateString := request.QueryParameter("dateFrom"); dateString != "" {
		date, err = time.Parse(time.RFC3339, dateString)
		if err != nil {
			addPlainTextError(response, http.StatusBadRequest, fmt.Sprint(err.Error(), " - Correct format is RFC3339"))
			return
		}
	} else {
		date = time.Time{}
	}

	q := datastore.NewQuery(chartDBEntity).Filter("Header.LastChanged >=", date).Order("Header.LastChanged").Limit(200)

	var chartHeaderList ChartAPIv1HeaderOnlyList

	var chartsOnDBList []ChartEntityHeaderOnly
	k, err := q.GetAll(c, &chartsOnDBList)
	if err != nil && !isErrFieldMismatch(err) {
		if appengine.IsOverQuota(err) {
			// return 503 and a text similar to what GAE is returning as well
			addPlainTextError(response, http.StatusServiceUnavailable, "503 - Over Quota")
		} else {
			addPlainTextError(response, http.StatusInternalServerError, err.Error())
		}
		return
	}

	// DB Entity needs to be mapped back
	for i, chartDB := range chartsOnDBList {
		var chart ChartAPIv1HeaderOnly
		mapDBtoAPIChartHeader(&chartDB.Header, &chart.Header)
		chart.Header.Id = k[i].IntID()
		chartHeaderList = append(chartHeaderList, chart)
	}

	response.WriteHeaderAndEntity(http.StatusOK, chartHeaderList)

}

func getChartHeaderCount(request *restful.Request, response *restful.Response) {
	c := appengine.NewContext(request.Request)

	var date time.Time
	var err  error
	if dateString := request.QueryParameter("dateFrom"); dateString != "" {
		date, err = time.Parse(time.RFC3339, dateString)
		if err != nil {
			addPlainTextError(response, http.StatusBadRequest, fmt.Sprint(err.Error(), " - Correct format is RFC3339"))
			return
		}
	} else {
		date = time.Time{}
	}

	q:= datastore.NewQuery(chartDBEntity).Filter("Header.LastChanged >=", date  ).Order("-Header.LastChanged")
	counter, _ := q.Count(c)

	response.WriteHeaderAndEntity(http.StatusOK, counter)

}

func getChartById(request *restful.Request, response *restful.Response) {
	c := appengine.NewContext(request.Request)

	id := request.PathParameter("id")
	i, err := strconv.ParseInt(id, 10, 64)
	if err != nil {
		addPlainTextError(response, http.StatusBadRequest, err.Error())
		return
	}

	key := datastore.NewKey(c, chartDBEntity, "", i, chartEntityRootKey(c))

	chartDB := new(ChartEntity)
	err = datastore.Get(c, key, chartDB)
	if err != nil && !isErrFieldMismatch(err) {
		switch {
		case appengine.IsOverQuota(err):
			// return 503 and a text similar to what GAE is returning as well
			addPlainTextError(response, http.StatusServiceUnavailable, "503 - Over Quota")
		case err == datastore.ErrNoSuchEntity:
			addPlainTextError(response, http.StatusNotFound, err.Error())
		default:
			addPlainTextError(response, http.StatusBadRequest, err.Error())
		}
		return
	}

	// now map and respond
	chart := new(ChartAPIv1)
	mapDBtoAPIChart(chartDB, chart)
	chart.Header.Id = key.IntID()

	response.WriteHeaderAndEntity(http.StatusOK, chart)
}

func deleteChartById(request *restful.Request, response *restful.Response) {

	changeChartById(request, response, true, false, true)

}

func curateChartById(request *restful.Request, response *restful.Response) {

	newStatusString := request.QueryParameter("newStatus")
	b, err := strconv.ParseBool(newStatusString)
	if err != nil {
		addPlainTextError(response, http.StatusBadRequest, err.Error())
		return
	}
	changeChartById(request, response, false, true, b)

}

// ------------------- supporting functions ------------------------------------------------

func changeChartById(request *restful.Request, response *restful.Response, changeDeleted bool, changeCurated bool, newStatus bool) {
	c := appengine.NewContext(request.Request)

	id := request.PathParameter("id")
	i, err := strconv.ParseInt(id, 10, 64)
	if err != nil {
		addPlainTextError(response, http.StatusBadRequest, err.Error())
		return
	}

	key := datastore.NewKey(c, chartDBEntity, "", i, chartEntityRootKey(c))

	chartDB := new(ChartEntity)
	err = datastore.Get(c, key, chartDB)
	if err != nil && !isErrFieldMismatch(err) {
		switch {
		case appengine.IsOverQuota(err):
			// return 503 and a text similar to what GAE is returning as well
			addPlainTextError(response, http.StatusServiceUnavailable, "503 - Over Quota")
		case err == datastore.ErrNoSuchEntity:
			addPlainTextError(response, http.StatusNotFound, err.Error())
		default:
			addPlainTextError(response, http.StatusBadRequest, err.Error())
		}
		return
	}

	// now update like requested

	if changeDeleted {
		chartDB.Header.Deleted = newStatus
		if newStatus {
			chartDB.ChartXML = ""
			chartDB.Image = nil
		}
		chartDB.Header.LastChanged = time.Now()
	}

	if changeCurated {
		chartDB.Header.Curated = newStatus
		chartDB.Header.LastChanged = time.Now()
	}

	if 	_, err := datastore.Put(c, key, chartDB); err != nil {
		if appengine.IsOverQuota(err) {
			// return 503 and a text similar to what GAE is returning as well
			addPlainTextError(response, http.StatusServiceUnavailable, "503 - Over Quota")
		} else {
			addPlainTextError(response, http.StatusInternalServerError, err.Error())
		}
		return
	}

	// send back the key
	response.WriteHeaderAndEntity(http.StatusNoContent, strconv.FormatInt(key.IntID(), 10))

}

// ignore missing fields error when mapping to Header struct
func isErrFieldMismatch(err error) bool {
	_, ok := err.(*datastore.ErrFieldMismatch)
	return ok
}<commit_msg>Don't use "magic" number, but a 'const'<commit_after>/*
 * Copyright (c) 2015 Joern Rischmueller (joern.rm@gmail.com)
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Affero General Public License as
 *  published by the Free Software Foundation, either version 3 of the
 *  License, or (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Affero General Public License for more details.
 *
 *  You should have received a copy of the GNU Affero General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package goldencheetah

import (
	"net/http"
	"time"
	"strconv"
	"fmt"

    "golang.org/x/net/context"
	"google.golang.org/appengine"
	"google.golang.org/appengine/datastore"

	b64 "encoding/base64"

	"github.com/emicklei/go-restful"

)

// ---------------------------------------------------------------------------------------------------------------//
// Full Golden Cheetah chart definition (chartentity) which is stored in DB
// ---------------------------------------------------------------------------------------------------------------//
type ChartEntity struct {
	Header          ChartEntityHeader
	ChartXML	    string       `datastore:",noindex"`
	Image			[]byte       `datastore:",noindex"`
	CreatorNick		string       `datastore:",noindex"`
	CreatorEmail	string       `datastore:",noindex"`
}

type ChartEntityHeaderOnly struct {
	Header          ChartEntityHeader
}

type ChartEntityHeader struct {
	Name 	   		string       `datastore:",noindex"`
	Description 	string       `datastore:",noindex"`
	Language		string       `datastore:",noindex"`
	GcVersion 		string
	LastChanged 	time.Time
	CreatorId		string
	Curated			bool
	Deleted         bool
}

// ---------------------------------------------------------------------------------------------------------------//
// API View Definition
// ---------------------------------------------------------------------------------------------------------------//

// Full structure for GET and PUT
type ChartAPIv1 struct {
	Header          ChartAPIHeaderV1 `json:"header"`
	ChartXML        string      `json:"chartxml"`
	Image           string      `json:"image"`
	CreatorNick     string      `json:"creatorNick"`
	CreatorEmail    string      `json:"creatorEmail"`

}

type ChartAPIv1List []ChartAPIv1

// Header only structure
type ChartAPIv1HeaderOnly struct {
	Header          ChartAPIHeaderV1 `json:"header"`
}
type ChartAPIv1HeaderOnlyList []ChartAPIv1HeaderOnly

// Internal Structure for Header
type ChartAPIHeaderV1 struct {
	Id				int64		`json:"id"`
	Name            string   	`json:"name"`
	Description     string      `json:"description"`
	GcVersion       string      `json:"gcversion"`
	LastChanged     string      `json:"lastChange"`
	CreatorId       string      `json:"creatorId"`
	Language        string      `json:"language"`
	Curated			bool		`json:"curated"`
	Deleted			bool		`json:"deleted"`
}

// ---------------------------------------------------------------------------------------------------------------//
// Data Storage View
// ---------------------------------------------------------------------------------------------------------------//

const chartDBEntity = "chartentity"
const chartDBEntityRootKey = "chartsroot"

func mapAPItoDBChart(api *ChartAPIv1, db *ChartEntity) {
	mapAPItoDBChartHeader(&api.Header, &db.Header)
	db.ChartXML = api.ChartXML
	data, err := b64.StdEncoding.DecodeString(api.Image)
	if err != nil{
		data = nil
	} else {
		db.Image = data
	}
	db.CreatorNick = api.CreatorNick
	db.CreatorEmail = api.CreatorEmail
}

func mapAPItoDBChartHeader(api *ChartAPIHeaderV1, db *ChartEntityHeader) {
	db.Name = api.Name
	db.Description = api.Description
	db.Language = api.Language
	db.GcVersion = api.GcVersion
	db.LastChanged, _ = time.Parse(dateTimeLayout, api.LastChanged)
	db.CreatorId = api.CreatorId
	db.Curated = api.Curated
	db.Deleted = api.Deleted
}

func mapDBtoAPIChart(db *ChartEntity, api *ChartAPIv1 ) {
	mapDBtoAPIChartHeader(&db.Header, &api.Header)
	api.ChartXML = db.ChartXML
	api.Image = b64.StdEncoding.EncodeToString(db.Image)
	api.CreatorNick = db.CreatorNick
	api.CreatorEmail = db.CreatorEmail
}

func mapDBtoAPIChartHeader(db *ChartEntityHeader, api *ChartAPIHeaderV1 ) {
	api.Name = db.Name
	api.Description = db.Description
	api.GcVersion = db.GcVersion
	api.Language = db.Language
	api.LastChanged = db.LastChanged.Format(dateTimeLayout)
	api.CreatorId = db.CreatorId
	api.Curated = db.Curated
	api.Deleted = db.Deleted
}

// supporting functions

// chartEntityKey returns the key used for all chartEntity entries.
func chartEntityRootKey(c context.Context) *datastore.Key {
	return datastore.NewKey(c, chartDBEntity, chartDBEntityRootKey, 0, nil)
}

// ---------------------------------------------------------------------------------------------------------------//
// request/response handler
// ---------------------------------------------------------------------------------------------------------------//

func insertChart(request *restful.Request, response *restful.Response) {
	c := appengine.NewContext(request.Request)

	chart := new(ChartAPIv1)
	if err := request.ReadEntity(chart); err != nil {
		addPlainTextError(response,http.StatusInternalServerError, err.Error())
		return
	}

	// No checks if the necessary fields are filed or not - since GoldenCheetah is
	// the only consumer of the APIs - any checks/response are to support this use-case

	chartDB := new(ChartEntity)
	mapAPItoDBChart(chart, chartDB)

	// complete/set POST fields
	chartDB.Header.LastChanged = time.Now()
	chartDB.Header.Curated = false
	chartDB.Header.Deleted = false

	// auto-curate if a registered "curator" is adding a chart
	curatorQuery := datastore.NewQuery(curatorDBEntity).Filter("CuratorId =", chartDB.Header.CreatorId).KeysOnly()
	var curatorOnDBList []CuratorEntity
	_, cErr := curatorQuery.GetAll(c, &curatorOnDBList)
	if cErr != nil {
		chartDB.Header.Curated = false
	} else {
		chartDB.Header.Curated = true
	}

	// and now store it
	key := datastore.NewIncompleteKey(c, chartDBEntity, chartEntityRootKey(c))
	key, err := datastore.Put(c, key, chartDB);
	if  err != nil {
		if appengine.IsOverQuota(err) {
			// return 503 and a text similar to what GAE is returning as well
			addPlainTextError(response, http.StatusServiceUnavailable, "503 - Over Quota")
		} else {
			addPlainTextError(response, http.StatusInternalServerError, err.Error())
		}
		return
	}

	// send back the key
	response.WriteHeaderAndEntity(http.StatusCreated, strconv.FormatInt(key.IntID(), 10))

}

func updateChart(request *restful.Request, response *restful.Response) {
	c := appengine.NewContext(request.Request)

	chart := new(ChartAPIv1)
	if 	err := request.ReadEntity(chart); err != nil {
		addPlainTextError(response, http.StatusInternalServerError, err.Error())
		return
	}

	if (chart.Header.Id == 0) {
		addPlainTextError(response, http.StatusBadRequest, "Mandatory Id/Key for Update is missing or invalid")
		return
	}

	// No more checks if the necessary fields are filed or not - since GoldenCheetah is
	// the only consumer of the APIs - any checks/response are to support this use-case

	chartDB := new(ChartEntity)
	mapAPItoDBChart(chart, chartDB)

	chartDB.Header.LastChanged = time.Now()

	// and now store it

	key := datastore.NewKey(c, chartDBEntity, "", chart.Header.Id, chartEntityRootKey(c))
	if 	_, err := datastore.Put(c, key, chartDB); err != nil {
		if appengine.IsOverQuota(err) {
			// return 503 and a text similar to what GAE is returning as well
			addPlainTextError(response, http.StatusServiceUnavailable, "503 - Over Quota")
		} else {
			addPlainTextError(response, http.StatusInternalServerError, err.Error())
		}
		return
	}

	// send back the key
	response.WriteHeaderAndEntity(http.StatusNoContent, strconv.FormatInt(key.IntID(), 10))

}
func getChartHeader(request *restful.Request, response *restful.Response) {
	c := appengine.NewContext(request.Request)

	var date time.Time
	var err  error
	if dateString := request.QueryParameter("dateFrom"); dateString != "" {
		date, err = time.Parse(time.RFC3339, dateString)
		if err != nil {
			addPlainTextError(response, http.StatusBadRequest, fmt.Sprint(err.Error(), " - Correct format is RFC3339"))
			return
		}
	} else {
		date = time.Time{}
	}

	const maxNumberOfHeadersPerCall = 200; // this has to be equal to GoldenCheetah - CloudDBChartClient class

	q := datastore.NewQuery(chartDBEntity).Filter("Header.LastChanged >=", date).Order("Header.LastChanged").Limit(maxNumberOfHeadersPerCall)

	var chartHeaderList ChartAPIv1HeaderOnlyList

	var chartsOnDBList []ChartEntityHeaderOnly
	k, err := q.GetAll(c, &chartsOnDBList)
	if err != nil && !isErrFieldMismatch(err) {
		if appengine.IsOverQuota(err) {
			// return 503 and a text similar to what GAE is returning as well
			addPlainTextError(response, http.StatusServiceUnavailable, "503 - Over Quota")
		} else {
			addPlainTextError(response, http.StatusInternalServerError, err.Error())
		}
		return
	}

	// DB Entity needs to be mapped back
	for i, chartDB := range chartsOnDBList {
		var chart ChartAPIv1HeaderOnly
		mapDBtoAPIChartHeader(&chartDB.Header, &chart.Header)
		chart.Header.Id = k[i].IntID()
		chartHeaderList = append(chartHeaderList, chart)
	}

	response.WriteHeaderAndEntity(http.StatusOK, chartHeaderList)

}

func getChartHeaderCount(request *restful.Request, response *restful.Response) {
	c := appengine.NewContext(request.Request)

	var date time.Time
	var err  error
	if dateString := request.QueryParameter("dateFrom"); dateString != "" {
		date, err = time.Parse(time.RFC3339, dateString)
		if err != nil {
			addPlainTextError(response, http.StatusBadRequest, fmt.Sprint(err.Error(), " - Correct format is RFC3339"))
			return
		}
	} else {
		date = time.Time{}
	}

	q:= datastore.NewQuery(chartDBEntity).Filter("Header.LastChanged >=", date  ).Order("-Header.LastChanged")
	counter, _ := q.Count(c)

	response.WriteHeaderAndEntity(http.StatusOK, counter)

}

func getChartById(request *restful.Request, response *restful.Response) {
	c := appengine.NewContext(request.Request)

	id := request.PathParameter("id")
	i, err := strconv.ParseInt(id, 10, 64)
	if err != nil {
		addPlainTextError(response, http.StatusBadRequest, err.Error())
		return
	}

	key := datastore.NewKey(c, chartDBEntity, "", i, chartEntityRootKey(c))

	chartDB := new(ChartEntity)
	err = datastore.Get(c, key, chartDB)
	if err != nil && !isErrFieldMismatch(err) {
		switch {
		case appengine.IsOverQuota(err):
			// return 503 and a text similar to what GAE is returning as well
			addPlainTextError(response, http.StatusServiceUnavailable, "503 - Over Quota")
		case err == datastore.ErrNoSuchEntity:
			addPlainTextError(response, http.StatusNotFound, err.Error())
		default:
			addPlainTextError(response, http.StatusBadRequest, err.Error())
		}
		return
	}

	// now map and respond
	chart := new(ChartAPIv1)
	mapDBtoAPIChart(chartDB, chart)
	chart.Header.Id = key.IntID()

	response.WriteHeaderAndEntity(http.StatusOK, chart)
}

func deleteChartById(request *restful.Request, response *restful.Response) {

	changeChartById(request, response, true, false, true)

}

func curateChartById(request *restful.Request, response *restful.Response) {

	newStatusString := request.QueryParameter("newStatus")
	b, err := strconv.ParseBool(newStatusString)
	if err != nil {
		addPlainTextError(response, http.StatusBadRequest, err.Error())
		return
	}
	changeChartById(request, response, false, true, b)

}

// ------------------- supporting functions ------------------------------------------------

func changeChartById(request *restful.Request, response *restful.Response, changeDeleted bool, changeCurated bool, newStatus bool) {
	c := appengine.NewContext(request.Request)

	id := request.PathParameter("id")
	i, err := strconv.ParseInt(id, 10, 64)
	if err != nil {
		addPlainTextError(response, http.StatusBadRequest, err.Error())
		return
	}

	key := datastore.NewKey(c, chartDBEntity, "", i, chartEntityRootKey(c))

	chartDB := new(ChartEntity)
	err = datastore.Get(c, key, chartDB)
	if err != nil && !isErrFieldMismatch(err) {
		switch {
		case appengine.IsOverQuota(err):
			// return 503 and a text similar to what GAE is returning as well
			addPlainTextError(response, http.StatusServiceUnavailable, "503 - Over Quota")
		case err == datastore.ErrNoSuchEntity:
			addPlainTextError(response, http.StatusNotFound, err.Error())
		default:
			addPlainTextError(response, http.StatusBadRequest, err.Error())
		}
		return
	}

	// now update like requested

	if changeDeleted {
		chartDB.Header.Deleted = newStatus
		if newStatus {
			chartDB.ChartXML = ""
			chartDB.Image = nil
		}
		chartDB.Header.LastChanged = time.Now()
	}

	if changeCurated {
		chartDB.Header.Curated = newStatus
		chartDB.Header.LastChanged = time.Now()
	}

	if 	_, err := datastore.Put(c, key, chartDB); err != nil {
		if appengine.IsOverQuota(err) {
			// return 503 and a text similar to what GAE is returning as well
			addPlainTextError(response, http.StatusServiceUnavailable, "503 - Over Quota")
		} else {
			addPlainTextError(response, http.StatusInternalServerError, err.Error())
		}
		return
	}

	// send back the key
	response.WriteHeaderAndEntity(http.StatusNoContent, strconv.FormatInt(key.IntID(), 10))

}

// ignore missing fields error when mapping to Header struct
func isErrFieldMismatch(err error) bool {
	_, ok := err.(*datastore.ErrFieldMismatch)
	return ok
}<|endoftext|>


<commit_before>// 29 december 2012
package main

import (
	"fmt"
	"os"
	"io"
	"path/filepath"
	"archive/zip"
	"strconv"
	"crypto/sha1"
	"encoding/hex"
	"bytes"
	"log"
)

var sha1hash = sha1.New()

func crc32match(zipcrc uint32, gamecrc string) bool {
	if gamecrc == "" {	// assume lack of CRC32 means do not check
		return true
	}
	n, err := strconv.ParseUint(gamecrc, 16, 32)
	if err != nil {
		log.Fatalf("string convert error reading crc32 (%q): %v", gamecrc, err)
	}
	return uint32(n) == zipcrc
}

func sha1check(zf *zip.File, expectstring string) (bool, error) {
	expected, err := hex.DecodeString(expectstring)
	if err != nil {
		log.Fatalf("hex decode error reading sha1 (%q): %v", expectstring, err)
	}

	f, err := zf.Open()
	if err != nil {
		return false, fmt.Errorf("could not open given zip file entry: %v", err)
	}
	defer f.Close()

	sha1hash.Reset()
	n, err := io.Copy(sha1hash, f)
	if err != nil {
		return false, fmt.Errorf("could not read given zip file entry: %v", err)
	}
	// TODO could we have integer size/signedness conversion failure here? zf.UncompressedSize is not an int64
	if n != int64(zf.UncompressedSize) {
		return false, fmt.Errorf("short read from zip file or write to hash but no error returned (expected %d bytes; got %d)", int64(zf.UncompressedSize), n)
	}

	return bytes.Equal(expected, sha1hash.Sum(nil)), nil
}

func (g *Game) Filename(rompath string) string {
	return filepath.Join(rompath, g.Name + ".zip")
}

func (g *Game) checkOneZip(zipname string, roms map[string]*ROM, isParent bool) (bool, error) {
	f, err := zip.OpenReader(zipname)
	if os.IsNotExist(err) {		// if the file does not exist, try the next rompath
		return false, nil
	}
	if err != nil {			// something different happened
		return false, fmt.Errorf("could not open zip file %s: %v", zipname, err)
	}
	defer f.Close()

	for _, file := range f.File {
		rom, ok := roms[file.Name]
		if !ok {				// not in archive
			if isParent {		// if we're in a parent, we already walked over this file in the clone (or this file has a different name in the clone)
				continue
			}
			return false, nil		// otherwise we have a problem
		}
		if file.UncompressedSize != rom.Size {
			return false, nil
		}
		if !crc32match(file.CRC32, rom.CRC32) {
			return false, nil
		}
		good, err := sha1check(file, rom.SHA1)
		if err != nil {
			return false, fmt.Errorf("could not calculate SHA-1 sum of %s in %s: %v", g.Name, zipname, err)
		}
		if !good {
			return false, nil
		}
		delete(roms, file.Name)		// mark as done
	}

	return true, nil					// all clear on this one
}

func (g *Game) CheckIn(rompath string) (bool, error) {
	// populate list of ROMs
	var roms = make(map[string]*ROM)
	for i := range g.ROMs {
		if g.ROMs[i].Status != nodump {	// otherwise games with known undumped ROMs will return "not found" because the map never depletes
			roms[g.ROMs[i].Name] = &(g.ROMs[i])
		}
	}

	zipname := g.Filename(rompath)
	good, err := g.checkOneZip(zipname, roms, false)
	if err != nil {
		return false, err
	} else if !good {
		return false, nil
	}

	// TODO eliminate reptition from this and Find()?
	if g.CloneOf != "" {
		if optimal[g.CloneOf] == "" {		// if we reached here it should have been found
			return false, nil
		}
		k := games[g.CloneOf]
		good, err := k.checkOneZip(optimal[g.CloneOf], roms, true)
		if err != nil {
			return false, err
		} else if !good {
			return false, nil
		}
	}
	if g.ROMOf != "" && g.ROMOf != g.CloneOf {
		if optimal[g.ROMOf] == "" {		// if we reached here it should have been found
			return false, nil
		}
		k := games[g.ROMOf]
		good, err := k.checkOneZip(optimal[g.ROMOf], roms, true)
		if err != nil {
			return false, err
		} else if !good {
			return false, nil
		}
	}

	// if we reached here everything we know about checked out, so if there are any leftover files in the game, that means something is wrong
	return len(roms) == 0, nil
}

func (g *Game) Find() (found bool, err error) {
	// did we find this already?
	if optimal[g.Name] != "" {
		return true, nil
	}

	// find the parents
	if g.CloneOf != "" {
		found, err := games[g.CloneOf].Find()
		if err != nil {
			return false, fmt.Errorf("error finding parent (cloneof) %s: %v", g.CloneOf, err)
		}
		// TODO really bail out?
		if !found {
			return false, err
		}
	}
	if g.ROMOf != "" && g.ROMOf != g.CloneOf {
		found, err := games[g.ROMOf].Find()
		if err != nil {
			return false, fmt.Errorf("error finding parent (romof) %s: %v", g.ROMOf, err)
		}
		// TODO really bail out?
		if !found {
			return false, err
		}
	}

	// go through the directories
	for _, d := range dirs {
		found, err := g.CheckIn(d)
		if err != nil {
			return false, err
		}
		if found {
			optimal[g.Name] = g.Filename(d)
			return true, nil
		}
	}

	// nope
	return false, nil
}
<commit_msg>Fixed handling of trailing (and consequently leading) whitespace in filenames specified in the XML.<commit_after>// 29 december 2012
package main

import (
	"fmt"
	"os"
	"io"
	"path/filepath"
	"archive/zip"
	"strconv"
	"crypto/sha1"
	"encoding/hex"
	"bytes"
	"strings"
	"log"
)

var sha1hash = sha1.New()

func crc32match(zipcrc uint32, gamecrc string) bool {
	if gamecrc == "" {	// assume lack of CRC32 means do not check
		return true
	}
	n, err := strconv.ParseUint(gamecrc, 16, 32)
	if err != nil {
		log.Fatalf("string convert error reading crc32 (%q): %v", gamecrc, err)
	}
	return uint32(n) == zipcrc
}

func sha1check(zf *zip.File, expectstring string) (bool, error) {
	expected, err := hex.DecodeString(expectstring)
	if err != nil {
		log.Fatalf("hex decode error reading sha1 (%q): %v", expectstring, err)
	}

	f, err := zf.Open()
	if err != nil {
		return false, fmt.Errorf("could not open given zip file entry: %v", err)
	}
	defer f.Close()

	sha1hash.Reset()
	n, err := io.Copy(sha1hash, f)
	if err != nil {
		return false, fmt.Errorf("could not read given zip file entry: %v", err)
	}
	// TODO could we have integer size/signedness conversion failure here? zf.UncompressedSize is not an int64
	if n != int64(zf.UncompressedSize) {
		return false, fmt.Errorf("short read from zip file or write to hash but no error returned (expected %d bytes; got %d)", int64(zf.UncompressedSize), n)
	}

	return bytes.Equal(expected, sha1hash.Sum(nil)), nil
}

func (g *Game) Filename(rompath string) string {
	return filepath.Join(rompath, g.Name + ".zip")
}

func (g *Game) checkOneZip(zipname string, roms map[string]*ROM, isParent bool) (bool, error) {
	f, err := zip.OpenReader(zipname)
	if os.IsNotExist(err) {		// if the file does not exist, try the next rompath
		return false, nil
	}
	if err != nil {			// something different happened
		return false, fmt.Errorf("could not open zip file %s: %v", zipname, err)
	}
	defer f.Close()

	for _, file := range f.File {
		rom, ok := roms[file.Name]
		if !ok {				// not in archive
			if isParent {		// if we're in a parent, we already walked over this file in the clone (or this file has a different name in the clone)
				continue
			}
			return false, nil		// otherwise we have a problem
		}
		if file.UncompressedSize != rom.Size {
			return false, nil
		}
		if !crc32match(file.CRC32, rom.CRC32) {
			return false, nil
		}
		good, err := sha1check(file, rom.SHA1)
		if err != nil {
			return false, fmt.Errorf("could not calculate SHA-1 sum of %s in %s: %v", g.Name, zipname, err)
		}
		if !good {
			return false, nil
		}
		delete(roms, file.Name)		// mark as done
	}

	return true, nil					// all clear on this one
}

func (g *Game) CheckIn(rompath string) (bool, error) {
	// populate list of ROMs
	var roms = make(map[string]*ROM)
	for i := range g.ROMs {
		if g.ROMs[i].Status != nodump {	// otherwise games with known undumped ROMs will return "not found" because the map never depletes
			// some ROM sets (scregg, for instance) have trailing spaces in the filenames given in he XML file (dc0.c6, in this example)
			// TODO this will also remove leading spaces; is that correct?
			roms[strings.TrimSpace(g.ROMs[i].Name)] = &(g.ROMs[i])
		}
	}

	zipname := g.Filename(rompath)
	good, err := g.checkOneZip(zipname, roms, false)
	if err != nil {
		return false, err
	} else if !good {
		return false, nil
	}

	// TODO eliminate reptition from this and Find()?
	if g.CloneOf != "" {
		if optimal[g.CloneOf] == "" {		// if we reached here it should have been found
			return false, nil
		}
		k := games[g.CloneOf]
		good, err := k.checkOneZip(optimal[g.CloneOf], roms, true)
		if err != nil {
			return false, err
		} else if !good {
			return false, nil
		}
	}
	if g.ROMOf != "" && g.ROMOf != g.CloneOf {
		if optimal[g.ROMOf] == "" {		// if we reached here it should have been found
			return false, nil
		}
		k := games[g.ROMOf]
		good, err := k.checkOneZip(optimal[g.ROMOf], roms, true)
		if err != nil {
			return false, err
		} else if !good {
			return false, nil
		}
	}

	// if we reached here everything we know about checked out, so if there are any leftover files in the game, that means something is wrong
	return len(roms) == 0, nil
}

func (g *Game) Find() (found bool, err error) {
	// did we find this already?
	if optimal[g.Name] != "" {
		return true, nil
	}

	// find the parents
	if g.CloneOf != "" {
		found, err := games[g.CloneOf].Find()
		if err != nil {
			return false, fmt.Errorf("error finding parent (cloneof) %s: %v", g.CloneOf, err)
		}
		// TODO really bail out?
		if !found {
			return false, err
		}
	}
	if g.ROMOf != "" && g.ROMOf != g.CloneOf {
		found, err := games[g.ROMOf].Find()
		if err != nil {
			return false, fmt.Errorf("error finding parent (romof) %s: %v", g.ROMOf, err)
		}
		// TODO really bail out?
		if !found {
			return false, err
		}
	}

	// go through the directories
	for _, d := range dirs {
		found, err := g.CheckIn(d)
		if err != nil {
			return false, err
		}
		if found {
			optimal[g.Name] = g.Filename(d)
			return true, nil
		}
	}

	// nope
	return false, nil
}
<|endoftext|>


<commit_before>package main

import (
	"bytes"
	"database/sql"
	"fmt"
	"io"
	"net/http"
	"os"
	"os/exec"
	"runtime"
	"strings"
	"time"
	"unicode"
	"unicode/utf8"

	_ "github.com/mattn/go-sqlite3"
	"golang.org/x/text/runes"
	"golang.org/x/text/transform"
	"golang.org/x/text/unicode/norm"
)

const (
	version = "0.1.5"
)

func NormalizeString(text string) string {
	t := transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC)
	s, _, _ := transform.String(t, text)
	return s
}

func rightPad2Len(str, pad string, lenght int) string {
	for {
		str += pad
		if utf8.RuneCountInString(str) > lenght {
			return str
		}
	}
}

func rightPad2LenNorm(s string, padStr string, overallLen int) string {
	s = NormalizeString(s)
	var padCountInt int
	padCountInt = 1 + ((overallLen - utf8.RuneCountInString(padStr)) / utf8.RuneCountInString(padStr))
	var retStr = s + strings.Repeat(padStr, padCountInt)
	return retStr[:overallLen]
}

func OutLess(output string) {
	var in io.WriteCloser
	var cmd *exec.Cmd
	os.Setenv("LESSCHARSET", "utf-8")
	os_name := runtime.GOOS
	switch os_name {
	case "windows":
		homedir, err := os.UserHomeDir()
		check(err)
		bin_less := fmt.Sprintf("%s\\scoop\\shims\\less.EXE", homedir)
		cmd = exec.Command(bin_less, "-S", "-R")
	case "darwin", "linux":
		cmd = exec.Command("/usr/bin/less", "-S", "-R")
	}
	in, _ = cmd.StdinPipe()
	cmd.Stdout = os.Stdout
	io.Copy(in, bytes.NewBufferString(output))
	in.Close()
	cmd.Run()
}

func containsStr(slice []string, item string) bool {
	set := make(map[string]struct{}, len(slice))
	for _, s := range slice {
		set[s] = struct{}{}
	}
	_, ok := set[item]
	return ok
}

func check(e error) {
	if e != nil {
		panic(e)
	}
}

func updateDB() {
	timeout := time.Duration(5 * time.Second)
	client := http.Client{
		Timeout: timeout,
	}
	resp, err := client.Get("https://github.com/mvrpl/Terminal-Cheat-Sheet/blob/master/cheat_sheets.db?raw=true")
	if err != nil {
		fmt.Println("Error updating database!")
		os.Exit(1)
	}
	home, _ := os.UserHomeDir()
	db, err := os.Create(home + string(os.PathSeparator) + ".cheat_sheets.db")
	check(err)
	defer db.Close()
	defer resp.Body.Close()
	io.Copy(db, resp.Body)
}

func help() {
	fmt.Println("version: " + version)
	use := []string{"chsht vim", "Example: Show cheat sheet for vim."}
	fmt.Println(strings.Join(use, "\n\t"))
	opt_update := []string{"chsht --update", "Update database, internet connection is required."}
	fmt.Println(strings.Join(opt_update, "\n\t"))
	opt_ls := []string{"chsht --list", "List sofwares with cheat sheets in database."}
	fmt.Println(strings.Join(opt_ls, "\n\t"))
}

func main() {
	home, _ := os.UserHomeDir()
	if _, err := os.Stat(home + string(os.PathSeparator) + ".cheat_sheets.db"); os.IsNotExist(err) {
		updateDB()
	}
	db, err := sql.Open("sqlite3", home+string(os.PathSeparator)+".cheat_sheets.db")
	check(err)
	dbtables, err := db.Query("select name from sqlite_master where type = 'table'")
	check(err)

	tables := []string{}

	for dbtables.Next() {
		var name string
		_ = dbtables.Scan(&name)
		tables = append(tables, name)
	}

	if len(os.Args) <= 1 {
		help()
		os.Exit(0)
	}

	if os.Args[1] == "--update" {
		updateDB()
		fmt.Println("Database updated!")
		os.Exit(0)
	}

	if os.Args[1] == "--version" {
		fmt.Println("version: " + version)
		os.Exit(0)
	}

	if os.Args[1] == "--help" {
		help()
		os.Exit(0)
	}

	if os.Args[1] == "--list" {
		fmt.Println("Available:")
		for _, soft := range tables {
			fmt.Println("* " + soft)
		}
		os.Exit(0)
	}

	if strings.HasPrefix(os.Args[1], "--") == true {
		fmt.Println("Invalid option. See: --help")
		os.Exit(0)
	}

	Program := os.Args[1]

	if containsStr(tables, Program) != true {
		fmt.Println("Sofware not in list.")
		os.Exit(0)
	}

	rows, err := db.Query("SELECT * FROM " + Program)
	check(err)

	var data map[string][]map[string]string
	data = make(map[string][]map[string]string)

	for rows.Next() {
		var command string
		var about string
		var session string
		_ = rows.Scan(&command, &about, &session)
		commands := map[string]string{
			command: about,
		}
		data[session] = append(data[session], commands)
	}

	var strData string
	for key, value := range data {
		strData += "+" + strings.Repeat("-", len(key)+2) + "+\n"
		strData += "| \033[1m" + strings.ToUpper(key) + "\033[0m |\n"
		strData += "+" + strings.Repeat("-", len(key)+2) + "+\n"
		cmdSize := 0
		descSize := 0
		var cmdsANDdescs map[string]string
		cmdsANDdescs = make(map[string]string)
		for _, v := range value {
			for cmd, desc := range v {
				if len(cmd) > cmdSize {
					cmdSize = utf8.RuneCountInString(cmd)
				}
				if len(desc) > descSize {
					descSize = utf8.RuneCountInString(desc)
				}
				cmdsANDdescs[cmd] = desc
			}
		}
		strData += "+" + strings.Repeat("-", cmdSize+2) + "+" + strings.Repeat("-", descSize+2) + "+\n"
		for k, v := range cmdsANDdescs {
			strData += rightPad2Len("| "+k, " ", cmdSize+2) + "| "
			strData += rightPad2LenNorm(v, " ", descSize) + " |\n"
			strData += "+" + strings.Repeat("-", cmdSize+2) + "+" + strings.Repeat("-", descSize+2) + "+\n"
		}
	}
	OutLess(strData)
}
<commit_msg>set env var linux/mac<commit_after>package main

import (
	"bytes"
	"database/sql"
	"fmt"
	"io"
	"net/http"
	"os"
	"os/exec"
	"runtime"
	"strings"
	"time"
	"unicode"
	"unicode/utf8"

	_ "github.com/mattn/go-sqlite3"
	"golang.org/x/text/runes"
	"golang.org/x/text/transform"
	"golang.org/x/text/unicode/norm"
)

const (
	version = "0.1.5"
)

func NormalizeString(text string) string {
	t := transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC)
	s, _, _ := transform.String(t, text)
	return s
}

func rightPad2Len(str, pad string, lenght int) string {
	for {
		str += pad
		if utf8.RuneCountInString(str) > lenght {
			return str
		}
	}
}

func rightPad2LenNorm(s string, padStr string, overallLen int) string {
	s = NormalizeString(s)
	var padCountInt int
	padCountInt = 1 + ((overallLen - utf8.RuneCountInString(padStr)) / utf8.RuneCountInString(padStr))
	var retStr = s + strings.Repeat(padStr, padCountInt)
	return retStr[:overallLen]
}

func OutLess(output string) {
	var in io.WriteCloser
	var cmd *exec.Cmd
	os_name := runtime.GOOS
	switch os_name {
	case "windows":
		homedir, err := os.UserHomeDir()
		check(err)
		bin_less := fmt.Sprintf("%s\\scoop\\shims\\less.EXE", homedir)
		cmd = exec.Command(bin_less, "-S", "-R")
	case "darwin", "linux":
		os.Setenv("LESSCHARSET", "utf-8")
		cmd = exec.Command("/usr/bin/less", "-S", "-R")
	}
	in, _ = cmd.StdinPipe()
	cmd.Stdout = os.Stdout
	io.Copy(in, bytes.NewBufferString(output))
	in.Close()
	cmd.Run()
}

func containsStr(slice []string, item string) bool {
	set := make(map[string]struct{}, len(slice))
	for _, s := range slice {
		set[s] = struct{}{}
	}
	_, ok := set[item]
	return ok
}

func check(e error) {
	if e != nil {
		panic(e)
	}
}

func updateDB() {
	timeout := time.Duration(5 * time.Second)
	client := http.Client{
		Timeout: timeout,
	}
	resp, err := client.Get("https://github.com/mvrpl/Terminal-Cheat-Sheet/blob/master/cheat_sheets.db?raw=true")
	if err != nil {
		fmt.Println("Error updating database!")
		os.Exit(1)
	}
	home, _ := os.UserHomeDir()
	db, err := os.Create(home + string(os.PathSeparator) + ".cheat_sheets.db")
	check(err)
	defer db.Close()
	defer resp.Body.Close()
	io.Copy(db, resp.Body)
}

func help() {
	fmt.Println("version: " + version)
	use := []string{"chsht vim", "Example: Show cheat sheet for vim."}
	fmt.Println(strings.Join(use, "\n\t"))
	opt_update := []string{"chsht --update", "Update database, internet connection is required."}
	fmt.Println(strings.Join(opt_update, "\n\t"))
	opt_ls := []string{"chsht --list", "List sofwares with cheat sheets in database."}
	fmt.Println(strings.Join(opt_ls, "\n\t"))
}

func main() {
	home, _ := os.UserHomeDir()
	if _, err := os.Stat(home + string(os.PathSeparator) + ".cheat_sheets.db"); os.IsNotExist(err) {
		updateDB()
	}
	db, err := sql.Open("sqlite3", home+string(os.PathSeparator)+".cheat_sheets.db")
	check(err)
	dbtables, err := db.Query("select name from sqlite_master where type = 'table'")
	check(err)

	tables := []string{}

	for dbtables.Next() {
		var name string
		_ = dbtables.Scan(&name)
		tables = append(tables, name)
	}

	if len(os.Args) <= 1 {
		help()
		os.Exit(0)
	}

	if os.Args[1] == "--update" {
		updateDB()
		fmt.Println("Database updated!")
		os.Exit(0)
	}

	if os.Args[1] == "--version" {
		fmt.Println("version: " + version)
		os.Exit(0)
	}

	if os.Args[1] == "--help" {
		help()
		os.Exit(0)
	}

	if os.Args[1] == "--list" {
		fmt.Println("Available:")
		for _, soft := range tables {
			fmt.Println("* " + soft)
		}
		os.Exit(0)
	}

	if strings.HasPrefix(os.Args[1], "--") == true {
		fmt.Println("Invalid option. See: --help")
		os.Exit(0)
	}

	Program := os.Args[1]

	if containsStr(tables, Program) != true {
		fmt.Println("Sofware not in list.")
		os.Exit(0)
	}

	rows, err := db.Query("SELECT * FROM " + Program)
	check(err)

	var data map[string][]map[string]string
	data = make(map[string][]map[string]string)

	for rows.Next() {
		var command string
		var about string
		var session string
		_ = rows.Scan(&command, &about, &session)
		commands := map[string]string{
			command: about,
		}
		data[session] = append(data[session], commands)
	}

	var strData string
	for key, value := range data {
		strData += "+" + strings.Repeat("-", len(key)+2) + "+\n"
		strData += "| \033[1m" + strings.ToUpper(key) + "\033[0m |\n"
		strData += "+" + strings.Repeat("-", len(key)+2) + "+\n"
		cmdSize := 0
		descSize := 0
		var cmdsANDdescs map[string]string
		cmdsANDdescs = make(map[string]string)
		for _, v := range value {
			for cmd, desc := range v {
				if len(cmd) > cmdSize {
					cmdSize = utf8.RuneCountInString(cmd)
				}
				if len(desc) > descSize {
					descSize = utf8.RuneCountInString(desc)
				}
				cmdsANDdescs[cmd] = desc
			}
		}
		strData += "+" + strings.Repeat("-", cmdSize+2) + "+" + strings.Repeat("-", descSize+2) + "+\n"
		for k, v := range cmdsANDdescs {
			strData += rightPad2Len("| "+k, " ", cmdSize+2) + "| "
			strData += rightPad2LenNorm(v, " ", descSize) + " |\n"
			strData += "+" + strings.Repeat("-", cmdSize+2) + "+" + strings.Repeat("-", descSize+2) + "+\n"
		}
	}
	OutLess(strData)
}
<|endoftext|>


<commit_before>// clock counts down to or up from a target time.
package main

import (
	"fmt"
	"os"
	"time"
)

func main() {
	target := time.Date(2017, 8, 1, 0, 0, 0, 0, time.Local)
	motto := "Just Go"
	printTargetTime(target, motto)
	exitOnEnterKey()

	var previous time.Time
	for {
		now := time.Now().Truncate(time.Second)
		if now != previous {
			previous = now
			countdown := now.Sub(target) // Negative times are before the target
			printCountdown(now, countdown)
		}
		time.Sleep(100 * time.Millisecond)
	}
}

func exitOnEnterKey() {
	go func() {
		buf := make([]byte, 1)
		_, _ = os.Stdin.Read(buf)
		os.Exit(0)
	}()
}

const (
	indent         = "\t"
	highlightStart = "\x1b[1;35m"
	highlightEnd   = "\x1b[0m"
)

func printTargetTime(target time.Time, motto string) {
	fmt.Print(indent, highlightStart, motto, highlightEnd, "\n")
	fmt.Print(indent, target.Format(time.UnixDate), "\n")
}

func printCountdown(now time.Time, countdown time.Duration) {
	var sign string
	if countdown >= 0 {
		sign = "+"
	} else {
		sign = "-"
		countdown = -countdown
	}

	days := int(countdown / (24 * time.Hour))
	countdown = countdown % (24 * time.Hour)

	fmt.Print(indent, now.Format(time.UnixDate), "  ", sign)
	if days > 0 {
		fmt.Print(days, "d")
	}
	fmt.Print(countdown, "          \r")
	os.Stdout.Sync()
}
<commit_msg>Go 1.9 on 9.1<commit_after>// clock counts down to or up from a target time.
package main

import (
	"fmt"
	"os"
	"time"
)

func main() {
	target := time.Date(2017, 9, 1, 0, 0, 0, 0, time.Local)
	motto := "Just Go"
	printTargetTime(target, motto)
	exitOnEnterKey()

	var previous time.Time
	for {
		now := time.Now().Truncate(time.Second)
		if now != previous {
			previous = now
			countdown := now.Sub(target) // Negative times are before the target
			printCountdown(now, countdown)
		}
		time.Sleep(100 * time.Millisecond)
	}
}

func exitOnEnterKey() {
	go func() {
		buf := make([]byte, 1)
		_, _ = os.Stdin.Read(buf)
		os.Exit(0)
	}()
}

const (
	indent         = "\t"
	highlightStart = "\x1b[1;35m"
	highlightEnd   = "\x1b[0m"
)

func printTargetTime(target time.Time, motto string) {
	fmt.Print(indent, highlightStart, motto, highlightEnd, "\n")
	fmt.Print(indent, target.Format(time.UnixDate), "\n")
}

func printCountdown(now time.Time, countdown time.Duration) {
	var sign string
	if countdown >= 0 {
		sign = "+"
	} else {
		sign = "-"
		countdown = -countdown
	}

	days := int(countdown / (24 * time.Hour))
	countdown = countdown % (24 * time.Hour)

	fmt.Print(indent, now.Format(time.UnixDate), "  ", sign)
	if days > 0 {
		fmt.Print(days, "d")
	}
	fmt.Print(countdown, "          \r")
	os.Stdout.Sync()
}
<|endoftext|>


<commit_before>package config

import (
	"bytes"
	"fmt"
	"io"
	"log"
	"os"
	"strings"

	"github.com/sirupsen/logrus"
)

// LogConfig configures the log levels
type LogConfig struct {
	Level  string `json:"level"`
	Format string `json:"format"`
	Output string `json:"output"`
}

var defaultLog = &LogConfig{
	Level:  "INFO",
	Format: "default",
	Output: "stdout",
}

func init() {
	if err := defaultLog.init(); err != nil {
		log.Println(err)
	}
}

func (l *LogConfig) init() error {
	// Set defaults
	if l.Level == "" {
		l.Level = defaultLog.Level
	}
	if l.Format == "" {
		l.Format = defaultLog.Format
	}
	if l.Output == "" {
		l.Output = defaultLog.Output
	}
	level, err := logrus.ParseLevel(strings.ToLower(l.Level))
	if err != nil {
		return fmt.Errorf("Unknown log level '%s': %s", l.Level, err)
	}
	var formatter logrus.Formatter
	var output io.Writer
	switch strings.ToLower(l.Format) {
	case "text":
		formatter = &logrus.TextFormatter{}
	case "json":
		formatter = &logrus.JSONFormatter{}
	case "default":
		formatter = &DefaultLogFormatter{}
	default:
		return fmt.Errorf("Unknown log format '%s'", l.Format)
	}
	switch strings.ToLower(l.Output) {
	case "stderr":
		output = os.Stderr
	case "stdout":
		output = os.Stdout
	default:
		return fmt.Errorf("Unknown output type '%s'", l.Output)
	}
	logrus.SetLevel(level)
	logrus.SetFormatter(formatter)
	logrus.SetOutput(output)
	return nil
}

// DefaultLogFormatter delegates formatting to standard go log package
type DefaultLogFormatter struct {
}

// Format formats the logrus entry by passing it to the "log" package
func (f *DefaultLogFormatter) Format(entry *logrus.Entry) ([]byte, error) {
	b := &bytes.Buffer{}
	logger := log.New(b, "", log.LstdFlags)
	logger.Println(entry.Message)
	// Panic and Fatal are handled by logrus automatically
	return b.Bytes(), nil
}
<commit_msg>Emit RFC3339 dates in default log format (#426)<commit_after>package config

import (
	"bytes"
	"fmt"
	"io"
	"time"
	"log"
	"os"
	"strings"

	"github.com/sirupsen/logrus"
)

// LogConfig configures the log levels
type LogConfig struct {
	Level  string `json:"level"`
	Format string `json:"format"`
	Output string `json:"output"`
}

var defaultLog = &LogConfig{
	Level:  "INFO",
	Format: "default",
	Output: "stdout",
}

func init() {
	if err := defaultLog.init(); err != nil {
		log.Println(err)
	}
}

func (l *LogConfig) init() error {
	// Set defaults
	if l.Level == "" {
		l.Level = defaultLog.Level
	}
	if l.Format == "" {
		l.Format = defaultLog.Format
	}
	if l.Output == "" {
		l.Output = defaultLog.Output
	}
	level, err := logrus.ParseLevel(strings.ToLower(l.Level))
	if err != nil {
		return fmt.Errorf("Unknown log level '%s': %s", l.Level, err)
	}
	var formatter logrus.Formatter
	var output io.Writer
	switch strings.ToLower(l.Format) {
	case "text":
		formatter = &logrus.TextFormatter{}
	case "json":
		formatter = &logrus.JSONFormatter{}
	case "default":
		formatter = &DefaultLogFormatter{}
	default:
		return fmt.Errorf("Unknown log format '%s'", l.Format)
	}
	switch strings.ToLower(l.Output) {
	case "stderr":
		output = os.Stderr
	case "stdout":
		output = os.Stdout
	default:
		return fmt.Errorf("Unknown output type '%s'", l.Output)
	}
	logrus.SetLevel(level)
	logrus.SetFormatter(formatter)
	logrus.SetOutput(output)
	return nil
}

// DefaultLogFormatter delegates formatting to standard go log package
type DefaultLogFormatter struct {
}

// RFC3339logWriter io.Writer that outputs RFC3339 dates
type RFC3339logWriter struct {
}

func (writer RFC3339logWriter) Write(bytes []byte) (int, error) {
	return fmt.Print(time.Now().Format(time.RFC3339Nano) + " " + string(bytes))
}

// Format formats the logrus entry by passing it to the "log" package
func (f *DefaultLogFormatter) Format(entry *logrus.Entry) ([]byte, error) {

	b := &bytes.Buffer{}
	logger := log.New(new(RFC3339logWriter), "", 0)
	logger.Println(entry.Message)
	// Panic and Fatal are handled by logrus automatically
	return b.Bytes(), nil
}
<|endoftext|>


<commit_before>// Copyright 2016 The etcd 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 clientv3

import (
	"log"
	"os"

	"go.etcd.io/etcd/client/pkg/v3/logutil"
	"go.uber.org/zap/zapcore"
	"go.uber.org/zap/zapgrpc"
	"google.golang.org/grpc/grpclog"
)

func init() {
	// We override grpc logger only when the environment variable is set
	// in order to not interfere by default with user's code or other libraries.
	if os.Getenv("ETCD_CLIENT_DEBUG") != "" {
		lg, err := logutil.CreateDefaultZapLogger(etcdClientDebugLevel())
		if err != nil {
			panic(err)
		}
		lg = lg.Named("etcd-client")
		grpclog.SetLoggerV2(zapgrpc.NewLogger(lg))
	}
}

// SetLogger sets grpc logger.
//
// Deprecated: use grpclog.SetLoggerV2 directly or grpc_zap.ReplaceGrpcLoggerV2.
func SetLogger(l grpclog.LoggerV2) {
	grpclog.SetLoggerV2(l)
}

// etcdClientDebugLevel translates ETCD_CLIENT_DEBUG into zap log level.
func etcdClientDebugLevel() zapcore.Level {
	envLevel := os.Getenv("ETCD_CLIENT_DEBUG")
	if envLevel == "" || envLevel == "true" {
		return zapcore.InfoLevel
	}
	var l zapcore.Level
	if err := l.Set(envLevel); err != nil {
		log.Printf("Deprecated env ETCD_CLIENT_DEBUG value. Using default level: 'info'")
		return zapcore.InfoLevel
	}
	return l
}
<commit_msg>Improve error message for incorrect values of ETCD_CLIENT_DEBUG<commit_after>// Copyright 2016 The etcd 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 clientv3

import (
	"log"
	"os"

	"go.etcd.io/etcd/client/pkg/v3/logutil"
	"go.uber.org/zap/zapcore"
	"go.uber.org/zap/zapgrpc"
	"google.golang.org/grpc/grpclog"
)

func init() {
	// We override grpc logger only when the environment variable is set
	// in order to not interfere by default with user's code or other libraries.
	if os.Getenv("ETCD_CLIENT_DEBUG") != "" {
		lg, err := logutil.CreateDefaultZapLogger(etcdClientDebugLevel())
		if err != nil {
			panic(err)
		}
		lg = lg.Named("etcd-client")
		grpclog.SetLoggerV2(zapgrpc.NewLogger(lg))
	}
}

// SetLogger sets grpc logger.
//
// Deprecated: use grpclog.SetLoggerV2 directly or grpc_zap.ReplaceGrpcLoggerV2.
func SetLogger(l grpclog.LoggerV2) {
	grpclog.SetLoggerV2(l)
}

// etcdClientDebugLevel translates ETCD_CLIENT_DEBUG into zap log level.
func etcdClientDebugLevel() zapcore.Level {
	envLevel := os.Getenv("ETCD_CLIENT_DEBUG")
	if envLevel == "" || envLevel == "true" {
		return zapcore.InfoLevel
	}
	var l zapcore.Level
	if err := l.Set(envLevel); err != nil {
		log.Printf("Invalid value for environment variable 'ETCD_CLIENT_DEBUG'. Using default level: 'info'")
		return zapcore.InfoLevel
	}
	return l
}
<|endoftext|>


<commit_before>// +build appengine

package gcache

import (
	"net/http"
	"time"

	"appengine"
	"golang.org/x/oauth2"
	"golang.org/x/oauth2/google"
	"google.golang.org/api/drive/v3"
	newappengine "google.golang.org/appengine"
	"google.golang.org/appengine/urlfetch"
)

const (
	deadline = time.Duration(60) * time.Second
)

func createGDriveClient(
	r *http.Request,
) *http.Client {
	c := appengine.NewContext(r)
	c = appengine.Timeout(c, deadline)
	ctx := newappengine.NewContext(r)

	if oauth2TokenSource == nil {
		oauth2TokenSource = google.AppEngineTokenSource(ctx, drive.DriveFileScope)
	}

	return &http.Client{
		Transport: &oauth2.Transport{
			Source: oauth2TokenSource,
			Base: &urlfetch.Transport{
				Context: ctx,
			},
		},
	}
}
<commit_msg>refactor<commit_after>// +build appengine

package gcache

import (
	"net/http"
	"time"

	"appengine"
	"golang.org/x/oauth2"
	"golang.org/x/oauth2/google"
	"google.golang.org/api/drive/v3"
	newappengine "google.golang.org/appengine"
	"google.golang.org/appengine/urlfetch"
)

const (
	deadline = time.Duration(60) * time.Second
)

func createGDriveClient(
	r *http.Request,
) *http.Client {
	c := appengine.Timeout(appengine.NewContext(r), deadline)
	ctx := newappengine.NewContext(r)

	if oauth2TokenSource == nil {
		oauth2TokenSource = google.AppEngineTokenSource(ctx, drive.DriveFileScope)
	}

	return &http.Client{
		Transport: &oauth2.Transport{
			Source: oauth2TokenSource,
			Base: &urlfetch.Transport{
				Context: ctx,
			},
		},
	}
}
<|endoftext|>


<commit_before>package main

import (
	"fmt"

	"github.com/projectatomic/buildah"
	"github.com/urfave/cli"
)

const (
	// DefaultRegistry is a prefix that we apply to an image name if we
	// can't find one in the local Store, in order to generate a source
	// reference for the image that we can then copy to the local Store.
	DefaultRegistry = "docker://"
)

var (
	fromFlags = []cli.Flag{
		cli.StringFlag{
			Name:  "name",
			Usage: "set a `name` for the working container",
		},
		cli.StringFlag{
			Name:  "image",
			Usage: "name of the starting `image`",
		},
		cli.BoolFlag{
			Name:  "pull",
			Usage: "pull the image if not present",
		},
		cli.BoolFlag{
			Name:  "pull-always",
			Usage: "pull the image even if one with the same name is already present",
		},
		cli.StringFlag{
			Name:  "registry",
			Usage: "`prefix` to prepend to the image name in order to pull the image",
			Value: DefaultRegistry,
		},
		cli.StringFlag{
			Name:  "signature-policy",
			Usage: "`pathname` of signature policy file (not usually used)",
		},
		cli.BoolFlag{
			Name:  "mount",
			Usage: "mount the working container",
		},
		cli.StringFlag{
			Name:  "link",
			Usage: "`pathname` of a symlink to create to the root directory of the container",
		},
	}
)

func fromCmd(c *cli.Context) error {
	image := ""
	if c.IsSet("image") {
		image = c.String("image")
	} else {
		return fmt.Errorf("an image name (or \"scratch\") must be specified")
	}
	registry := DefaultRegistry
	if c.IsSet("registry") {
		registry = c.String("registry")
	}
	pull := false
	if c.IsSet("pull") {
		pull = c.Bool("pull")
	}
	pullAlways := false
	if c.IsSet("pull-always") {
		pull = c.Bool("pull-always")
	}
	name := ""
	if c.IsSet("name") {
		name = c.String("name")
	}
	mount := false
	if c.IsSet("mount") {
		mount = c.Bool("mount")
	}
	link := ""
	if c.IsSet("link") {
		link = c.String("link")
		if link == "" {
			return fmt.Errorf("link location can not be empty")
		}
	}
	signaturePolicy := ""
	if c.IsSet("signature-policy") {
		signaturePolicy = c.String("signature-policy")
	}

	store, err := getStore(c)
	if err != nil {
		return err
	}

	options := buildah.BuilderOptions{
		FromImage:           image,
		Container:           name,
		PullIfMissing:       pull,
		PullAlways:          pullAlways,
		Mount:               mount,
		Link:                link,
		Registry:            registry,
		SignaturePolicyPath: signaturePolicy,
	}

	builder, err := buildah.NewBuilder(store, options)
	if err != nil {
		return err
	}

	fmt.Printf("%s\n", builder.Container)
	if options.Mount {
		fmt.Printf("%s\n", builder.MountPoint)
	}

	return builder.Save()
}
<commit_msg>Default "from" to --pull=true<commit_after>package main

import (
	"fmt"

	"github.com/projectatomic/buildah"
	"github.com/urfave/cli"
)

const (
	// DefaultRegistry is a prefix that we apply to an image name if we
	// can't find one in the local Store, in order to generate a source
	// reference for the image that we can then copy to the local Store.
	DefaultRegistry = "docker://"
)

var (
	fromFlags = []cli.Flag{
		cli.StringFlag{
			Name:  "name",
			Usage: "set a `name` for the working container",
		},
		cli.StringFlag{
			Name:  "image",
			Usage: "name of the starting `image`",
		},
		cli.BoolTFlag{
			Name:  "pull",
			Usage: "pull the image if not present",
		},
		cli.BoolFlag{
			Name:  "pull-always",
			Usage: "pull the image even if one with the same name is already present",
		},
		cli.StringFlag{
			Name:  "registry",
			Usage: "`prefix` to prepend to the image name in order to pull the image",
			Value: DefaultRegistry,
		},
		cli.StringFlag{
			Name:  "signature-policy",
			Usage: "`pathname` of signature policy file (not usually used)",
		},
		cli.BoolFlag{
			Name:  "mount",
			Usage: "mount the working container",
		},
		cli.StringFlag{
			Name:  "link",
			Usage: "`pathname` of a symlink to create to the root directory of the container",
		},
	}
)

func fromCmd(c *cli.Context) error {
	image := ""
	if c.IsSet("image") {
		image = c.String("image")
	} else {
		return fmt.Errorf("an image name (or \"scratch\") must be specified")
	}
	registry := DefaultRegistry
	if c.IsSet("registry") {
		registry = c.String("registry")
	}
	pull := true
	if c.IsSet("pull") {
		pull = c.BoolT("pull")
	}
	pullAlways := false
	if c.IsSet("pull-always") {
		pull = c.Bool("pull-always")
	}
	name := ""
	if c.IsSet("name") {
		name = c.String("name")
	}
	mount := false
	if c.IsSet("mount") {
		mount = c.Bool("mount")
	}
	link := ""
	if c.IsSet("link") {
		link = c.String("link")
		if link == "" {
			return fmt.Errorf("link location can not be empty")
		}
	}
	signaturePolicy := ""
	if c.IsSet("signature-policy") {
		signaturePolicy = c.String("signature-policy")
	}

	store, err := getStore(c)
	if err != nil {
		return err
	}

	options := buildah.BuilderOptions{
		FromImage:           image,
		Container:           name,
		PullIfMissing:       pull,
		PullAlways:          pullAlways,
		Mount:               mount,
		Link:                link,
		Registry:            registry,
		SignaturePolicyPath: signaturePolicy,
	}

	builder, err := buildah.NewBuilder(store, options)
	if err != nil {
		return err
	}

	fmt.Printf("%s\n", builder.Container)
	if options.Mount {
		fmt.Printf("%s\n", builder.MountPoint)
	}

	return builder.Save()
}
<|endoftext|>


<commit_before>/*
dokugen is a simple command line utility that exposes many of the basic functions of the
sudoku package. It's able to generate puzzles (with difficutly) and solve provided puzzles.
Run with -h to see help on how to use it.
*/
package main

import (
	"encoding/csv"
	"flag"
	"fmt"
	"github.com/gosuri/uiprogress"
	"github.com/jkomoros/sudoku"
	"github.com/jkomoros/sudoku/sdkconverter"
	"io"
	"io/ioutil"
	"log"
	"os"
	"path/filepath"
	"runtime"
	"strconv"
	"strings"
)

//TODO: let people pass in a filename to export to.

const STORED_PUZZLES_DIRECTORY = ".puzzles"

type appOptions struct {
	GENERATE            bool
	HELP                bool
	PUZZLE_TO_SOLVE     string
	NUM                 int
	PRINT_STATS         bool
	WALKTHROUGH         bool
	RAW_SYMMETRY        string
	RAW_DIFFICULTY      string
	SYMMETRY            sudoku.SymmetryType
	SYMMETRY_PROPORTION float64
	MIN_FILLED_CELLS    int
	MIN_DIFFICULTY      float64
	MAX_DIFFICULTY      float64
	NO_CACHE            bool
	PUZZLE_FORMAT       string
	OUTPUT_CSV          bool
	CONVERTER           sdkconverter.SudokuPuzzleConverter
	flagSet             *flag.FlagSet
}

var difficultyRanges map[string]struct {
	low, high float64
}

func init() {
	//grid.Difficulty can make use of a number of processes simultaneously.
	runtime.GOMAXPROCS(6)

	difficultyRanges = map[string]struct{ low, high float64 }{
		"gentle": {0.0, 0.3},
		"easy":   {0.3, 0.6},
		"medium": {0.6, 0.7},
		"tough":  {0.7, 1.0},
	}
}

func defineFlags(options *appOptions) {
	options.flagSet.BoolVar(&options.GENERATE, "g", false, "if true, will generate a puzzle.")
	options.flagSet.BoolVar(&options.HELP, "h", false, "If provided, will print help and exit.")
	options.flagSet.IntVar(&options.NUM, "n", 1, "Number of things to generate")
	options.flagSet.BoolVar(&options.PRINT_STATS, "p", false, "If provided, will print stats.")
	options.flagSet.StringVar(&options.PUZZLE_TO_SOLVE, "s", "", "If provided, will solve the puzzle at the given filename and print solution.")
	options.flagSet.BoolVar(&options.WALKTHROUGH, "w", false, "If provided, will print out a walkthrough to solve the provided puzzle.")
	options.flagSet.StringVar(&options.RAW_SYMMETRY, "y", "vertical", "Valid values: 'none', 'both', 'horizontal', 'vertical")
	options.flagSet.Float64Var(&options.SYMMETRY_PROPORTION, "r", 0.7, "What proportion of cells should be filled according to symmetry")
	options.flagSet.IntVar(&options.MIN_FILLED_CELLS, "min-filled-cells", 0, "The minimum number of cells that should be filled in the generated puzzles.")
	options.flagSet.Float64Var(&options.MIN_DIFFICULTY, "min", 0.0, "Minimum difficulty for generated puzzle")
	options.flagSet.Float64Var(&options.MAX_DIFFICULTY, "max", 1.0, "Maximum difficulty for generated puzzle")
	options.flagSet.BoolVar(&options.NO_CACHE, "no-cache", false, "If provided, will not vend generated puzzles from the cache of previously generated puzzles.")
	//TODO: the format should also be how we interpret loads, too.
	options.flagSet.StringVar(&options.PUZZLE_FORMAT, "format", "sdk", "Which format to export puzzles from. Defaults to 'sdk'")
	options.flagSet.BoolVar(&options.OUTPUT_CSV, "csv", false, "Output the results in CSV.")
	options.flagSet.StringVar(&options.RAW_DIFFICULTY, "d", "", "difficulty, one of {gentle, easy, medium, tough}")
}

func (o *appOptions) fixUp() {
	o.RAW_SYMMETRY = strings.ToLower(o.RAW_SYMMETRY)
	switch o.RAW_SYMMETRY {
	case "none":
		o.SYMMETRY = sudoku.SYMMETRY_NONE
	case "both":
		o.SYMMETRY = sudoku.SYMMETRY_BOTH
	case "horizontal":
		o.SYMMETRY = sudoku.SYMMETRY_HORIZONTAL
	case "vertical":
		o.SYMMETRY = sudoku.SYMMETRY_VERTICAL
	default:
		log.Fatal("Unknown symmetry flag: ", o.RAW_SYMMETRY)
	}

	o.RAW_DIFFICULTY = strings.ToLower(o.RAW_DIFFICULTY)
	if o.RAW_DIFFICULTY != "" {
		vals, ok := difficultyRanges[o.RAW_DIFFICULTY]
		if !ok {
			log.Fatal("Invalid difficulty option:", o.RAW_DIFFICULTY)
		}
		o.MIN_DIFFICULTY = vals.low
		o.MAX_DIFFICULTY = vals.high
		log.Println("Using difficulty max:", strconv.FormatFloat(vals.high, 'f', -1, 64), "min:", strconv.FormatFloat(vals.low, 'f', -1, 64))
	}

	o.CONVERTER = sdkconverter.Converters[o.PUZZLE_FORMAT]

	if o.CONVERTER == nil {
		log.Fatal("Invalid format option:", o.PUZZLE_FORMAT)
	}
}

func getOptions(flagSet *flag.FlagSet, flagArguments []string) *appOptions {
	options := &appOptions{flagSet: flagSet}
	defineFlags(options)
	flagSet.Parse(flagArguments)
	options.fixUp()
	return options
}

func main() {
	flagSet := flag.CommandLine
	process(getOptions(flagSet, os.Args[1:]), os.Stdout, os.Stderr)
}

func process(options *appOptions, output io.ReadWriter, errOutput io.ReadWriter) {

	if options.HELP {
		options.flagSet.PrintDefaults()
		return
	}

	var grid *sudoku.Grid

	var csvWriter *csv.Writer
	var csvRec []string

	if options.OUTPUT_CSV {
		csvWriter = csv.NewWriter(output)
	}

	var bar *uiprogress.Bar

	//TODO: do more useful / explanatory printing here.
	if options.NUM > 1 {
		uiprogress.DefaultProgress.Out = errOutput
		uiprogress.Start()
		bar = uiprogress.AddBar(options.NUM).PrependElapsed().AppendCompleted()
	}

	for i := 0; i < options.NUM; i++ {

		if options.OUTPUT_CSV {
			csvRec = nil
		}

		//TODO: allow the type of symmetry to be configured.
		if options.GENERATE {
			grid = generatePuzzle(options.MIN_DIFFICULTY, options.MAX_DIFFICULTY, options.SYMMETRY, options.SYMMETRY_PROPORTION, options.MIN_FILLED_CELLS, options.NO_CACHE)
			//TODO: factor out all of this double-printing.
			if options.OUTPUT_CSV {
				csvRec = append(csvRec, options.CONVERTER.DataString(grid))
			} else {
				fmt.Fprintln(output, options.CONVERTER.DataString(grid))
			}
		} else if options.PUZZLE_TO_SOLVE != "" {
			//TODO: detect if the load failed.
			grid = sudoku.NewGrid()

			data, err := ioutil.ReadFile(options.PUZZLE_TO_SOLVE)

			if err != nil {
				log.Fatalln("Read error for specified file:", err)
			}

			//TODO: shouldn't a load method have a way to say the string provided is invalid?
			options.CONVERTER.Load(grid, string(data))
		}

		if grid == nil {
			//No grid to do anything with.
			log.Fatalln("No grid loaded.")
		}

		//TODO: use of this option leads to a busy loop somewhere... Is it related to the generate-multiple-and-difficulty hang?

		var directions *sudoku.SolveDirections

		if options.WALKTHROUGH || options.PRINT_STATS {
			directions = grid.HumanSolution(nil)
			if len(directions.Steps) == 0 {
				//We couldn't solve it. Let's check and see if the puzzle is well formed.
				if grid.HasMultipleSolutions() {
					//TODO: figure out why guesses wouldn't be used here effectively.
					log.Println("The puzzle had multiple solutions; that means it's not well-formed")
				}
			}
		}

		if options.WALKTHROUGH {
			if options.OUTPUT_CSV {
				csvRec = append(csvRec, directions.Walkthrough())
			} else {
				fmt.Fprintln(output, directions.Walkthrough())
			}
		}
		if options.PRINT_STATS {
			if options.OUTPUT_CSV {
				csvRec = append(csvRec, strconv.FormatFloat(grid.Difficulty(), 'f', -1, 64))
				//We won't print out the directions.Stats() like we do for just printing to stdout,
				//because that's mostly noise in this format.
			} else {
				fmt.Fprintln(output, grid.Difficulty())
				//TODO: consider actually printing out the Signals stats (with a Stats method on signals)
				fmt.Fprintln(output, strings.Join(directions.Stats(), "\n"))
			}
		}
		if options.PUZZLE_TO_SOLVE != "" {
			grid.Solve()
			if options.OUTPUT_CSV {
				csvRec = append(csvRec, options.CONVERTER.DataString(grid))
			} else {
				fmt.Fprintln(output, options.CONVERTER.DataString(grid))

			}
		}

		if options.OUTPUT_CSV {
			csvWriter.Write(csvRec)
		}

		if options.PUZZLE_TO_SOLVE != "" {
			//If we're asked to solve, n could only be 1 anyway.
			return
		}
		grid.Done()
		if options.NUM > 1 {
			bar.Incr()
		}
	}
	if options.OUTPUT_CSV {
		csvWriter.Flush()
	}
}

func puzzleDirectoryParts(symmetryType sudoku.SymmetryType, symmetryPercentage float64, minFilledCells int) []string {
	return []string{
		STORED_PUZZLES_DIRECTORY,
		"SYM_TYPE_" + strconv.Itoa(int(symmetryType)),
		"SYM_PERCENTAGE_" + strconv.FormatFloat(symmetryPercentage, 'f', -1, 64),
		"MIN_FILED_CELLS_" + strconv.Itoa(minFilledCells),
	}
}

func storePuzzle(grid *sudoku.Grid, difficulty float64, symmetryType sudoku.SymmetryType, symmetryPercentage float64, minFilledCells int) bool {
	//TODO: we should include a hashed version of our difficulty weights file so we don't cache ones with old weights.
	directoryParts := puzzleDirectoryParts(symmetryType, symmetryPercentage, minFilledCells)

	fileNamePart := strconv.FormatFloat(difficulty, 'f', -1, 64) + ".sdk"

	pathSoFar := ""

	for i, part := range directoryParts {
		if i == 0 {
			pathSoFar = part
		} else {
			pathSoFar = filepath.Join(pathSoFar, part)
		}
		if _, err := os.Stat(pathSoFar); os.IsNotExist(err) {
			//need to create it.
			os.Mkdir(pathSoFar, 0700)
		}
	}

	fileName := filepath.Join(pathSoFar, fileNamePart)

	file, err := os.Create(fileName)

	if err != nil {
		log.Println(err)
		return false
	}

	defer file.Close()

	puzzleText := grid.DataString()

	n, err := io.WriteString(file, puzzleText)

	if err != nil {
		log.Println(err)
		return false
	} else {
		if n < len(puzzleText) {
			log.Println("Didn't write full file, only wrote", n, "bytes of", len(puzzleText))
			return false
		}
	}
	return true
}

func vendPuzzle(min float64, max float64, symmetryType sudoku.SymmetryType, symmetryPercentage float64, minFilledCells int) *sudoku.Grid {

	directory := filepath.Join(puzzleDirectoryParts(symmetryType, symmetryPercentage, minFilledCells)...)

	if files, err := ioutil.ReadDir(directory); os.IsNotExist(err) {
		//The directory doesn't exist.
		return nil
	} else {
		//OK, the directory exists, now see which puzzles are there and if any fit. If one does, vend it and delete the file.
		for _, file := range files {
			//See what this actually returns.
			filenameParts := strings.Split(file.Name(), ".")

			//Remember: there's a dot in the filename due to the float seperator.
			//TODO: shouldn't "sdk" be in a constant somewhere?
			if len(filenameParts) != 3 || filenameParts[2] != "sdk" {
				continue
			}

			difficulty, err := strconv.ParseFloat(strings.Join(filenameParts[0:2], "."), 64)
			if err != nil {
				continue
			}

			if min <= difficulty && difficulty <= max {
				//Found a puzzle!
				grid := sudoku.NewGrid()
				fullFileName := filepath.Join(directory, file.Name())
				grid.LoadFromFile(fullFileName)
				os.Remove(fullFileName)
				return grid
			}
		}
	}
	return nil
}

func generatePuzzle(min float64, max float64, symmetryType sudoku.SymmetryType, symmetryPercentage float64, minFilledCells int, skipCache bool) *sudoku.Grid {
	var result *sudoku.Grid

	if !skipCache {
		result = vendPuzzle(min, max, symmetryType, symmetryPercentage, minFilledCells)

		if result != nil {
			log.Println("Vending a puzzle from the cache.")
			return result
		}
	}

	options := sudoku.GenerationOptions{
		Symmetry:           symmetryType,
		SymmetryPercentage: symmetryPercentage,
		MinFilledCells:     minFilledCells,
	}

	//We'll have to generate one ourselves.
	count := 0
	for {
		//The first time we don't bother saying what number attemp it is, because if the first run is likely to generate a useable puzzle it's just noise.
		if count != 0 {
			log.Println("Attempt", count, "at generating puzzle.")
		}

		result = sudoku.GenerateGrid(&options)

		difficulty := result.Difficulty()

		if difficulty >= min && difficulty <= max {
			return result
		}

		log.Println("Rejecting grid of difficulty", difficulty)
		if storePuzzle(result, difficulty, symmetryType, symmetryPercentage, minFilledCells) {
			log.Println("Stored the puzzle for future use.")
		}

		count++
	}
	return nil
}
<commit_msg>Make it so when printing help process() uses the provided err output.<commit_after>/*
dokugen is a simple command line utility that exposes many of the basic functions of the
sudoku package. It's able to generate puzzles (with difficutly) and solve provided puzzles.
Run with -h to see help on how to use it.
*/
package main

import (
	"encoding/csv"
	"flag"
	"fmt"
	"github.com/gosuri/uiprogress"
	"github.com/jkomoros/sudoku"
	"github.com/jkomoros/sudoku/sdkconverter"
	"io"
	"io/ioutil"
	"log"
	"os"
	"path/filepath"
	"runtime"
	"strconv"
	"strings"
)

//TODO: let people pass in a filename to export to.

const STORED_PUZZLES_DIRECTORY = ".puzzles"

type appOptions struct {
	GENERATE            bool
	HELP                bool
	PUZZLE_TO_SOLVE     string
	NUM                 int
	PRINT_STATS         bool
	WALKTHROUGH         bool
	RAW_SYMMETRY        string
	RAW_DIFFICULTY      string
	SYMMETRY            sudoku.SymmetryType
	SYMMETRY_PROPORTION float64
	MIN_FILLED_CELLS    int
	MIN_DIFFICULTY      float64
	MAX_DIFFICULTY      float64
	NO_CACHE            bool
	PUZZLE_FORMAT       string
	OUTPUT_CSV          bool
	CONVERTER           sdkconverter.SudokuPuzzleConverter
	flagSet             *flag.FlagSet
}

var difficultyRanges map[string]struct {
	low, high float64
}

func init() {
	//grid.Difficulty can make use of a number of processes simultaneously.
	runtime.GOMAXPROCS(6)

	difficultyRanges = map[string]struct{ low, high float64 }{
		"gentle": {0.0, 0.3},
		"easy":   {0.3, 0.6},
		"medium": {0.6, 0.7},
		"tough":  {0.7, 1.0},
	}
}

func defineFlags(options *appOptions) {
	options.flagSet.BoolVar(&options.GENERATE, "g", false, "if true, will generate a puzzle.")
	options.flagSet.BoolVar(&options.HELP, "h", false, "If provided, will print help and exit.")
	options.flagSet.IntVar(&options.NUM, "n", 1, "Number of things to generate")
	options.flagSet.BoolVar(&options.PRINT_STATS, "p", false, "If provided, will print stats.")
	options.flagSet.StringVar(&options.PUZZLE_TO_SOLVE, "s", "", "If provided, will solve the puzzle at the given filename and print solution.")
	options.flagSet.BoolVar(&options.WALKTHROUGH, "w", false, "If provided, will print out a walkthrough to solve the provided puzzle.")
	options.flagSet.StringVar(&options.RAW_SYMMETRY, "y", "vertical", "Valid values: 'none', 'both', 'horizontal', 'vertical")
	options.flagSet.Float64Var(&options.SYMMETRY_PROPORTION, "r", 0.7, "What proportion of cells should be filled according to symmetry")
	options.flagSet.IntVar(&options.MIN_FILLED_CELLS, "min-filled-cells", 0, "The minimum number of cells that should be filled in the generated puzzles.")
	options.flagSet.Float64Var(&options.MIN_DIFFICULTY, "min", 0.0, "Minimum difficulty for generated puzzle")
	options.flagSet.Float64Var(&options.MAX_DIFFICULTY, "max", 1.0, "Maximum difficulty for generated puzzle")
	options.flagSet.BoolVar(&options.NO_CACHE, "no-cache", false, "If provided, will not vend generated puzzles from the cache of previously generated puzzles.")
	//TODO: the format should also be how we interpret loads, too.
	options.flagSet.StringVar(&options.PUZZLE_FORMAT, "format", "sdk", "Which format to export puzzles from. Defaults to 'sdk'")
	options.flagSet.BoolVar(&options.OUTPUT_CSV, "csv", false, "Output the results in CSV.")
	options.flagSet.StringVar(&options.RAW_DIFFICULTY, "d", "", "difficulty, one of {gentle, easy, medium, tough}")
}

func (o *appOptions) fixUp() {
	o.RAW_SYMMETRY = strings.ToLower(o.RAW_SYMMETRY)
	switch o.RAW_SYMMETRY {
	case "none":
		o.SYMMETRY = sudoku.SYMMETRY_NONE
	case "both":
		o.SYMMETRY = sudoku.SYMMETRY_BOTH
	case "horizontal":
		o.SYMMETRY = sudoku.SYMMETRY_HORIZONTAL
	case "vertical":
		o.SYMMETRY = sudoku.SYMMETRY_VERTICAL
	default:
		log.Fatal("Unknown symmetry flag: ", o.RAW_SYMMETRY)
	}

	o.RAW_DIFFICULTY = strings.ToLower(o.RAW_DIFFICULTY)
	if o.RAW_DIFFICULTY != "" {
		vals, ok := difficultyRanges[o.RAW_DIFFICULTY]
		if !ok {
			log.Fatal("Invalid difficulty option:", o.RAW_DIFFICULTY)
		}
		o.MIN_DIFFICULTY = vals.low
		o.MAX_DIFFICULTY = vals.high
		log.Println("Using difficulty max:", strconv.FormatFloat(vals.high, 'f', -1, 64), "min:", strconv.FormatFloat(vals.low, 'f', -1, 64))
	}

	o.CONVERTER = sdkconverter.Converters[o.PUZZLE_FORMAT]

	if o.CONVERTER == nil {
		log.Fatal("Invalid format option:", o.PUZZLE_FORMAT)
	}
}

func getOptions(flagSet *flag.FlagSet, flagArguments []string) *appOptions {
	options := &appOptions{flagSet: flagSet}
	defineFlags(options)
	flagSet.Parse(flagArguments)
	options.fixUp()
	return options
}

func main() {
	flagSet := flag.CommandLine
	process(getOptions(flagSet, os.Args[1:]), os.Stdout, os.Stderr)
}

func process(options *appOptions, output io.ReadWriter, errOutput io.ReadWriter) {

	options.flagSet.SetOutput(errOutput)

	if options.HELP {
		options.flagSet.PrintDefaults()
		return
	}

	var grid *sudoku.Grid

	var csvWriter *csv.Writer
	var csvRec []string

	if options.OUTPUT_CSV {
		csvWriter = csv.NewWriter(output)
	}

	var bar *uiprogress.Bar

	//TODO: do more useful / explanatory printing here.
	if options.NUM > 1 {
		uiprogress.DefaultProgress.Out = errOutput
		uiprogress.Start()
		bar = uiprogress.AddBar(options.NUM).PrependElapsed().AppendCompleted()
	}

	for i := 0; i < options.NUM; i++ {

		if options.OUTPUT_CSV {
			csvRec = nil
		}

		//TODO: allow the type of symmetry to be configured.
		if options.GENERATE {
			grid = generatePuzzle(options.MIN_DIFFICULTY, options.MAX_DIFFICULTY, options.SYMMETRY, options.SYMMETRY_PROPORTION, options.MIN_FILLED_CELLS, options.NO_CACHE)
			//TODO: factor out all of this double-printing.
			if options.OUTPUT_CSV {
				csvRec = append(csvRec, options.CONVERTER.DataString(grid))
			} else {
				fmt.Fprintln(output, options.CONVERTER.DataString(grid))
			}
		} else if options.PUZZLE_TO_SOLVE != "" {
			//TODO: detect if the load failed.
			grid = sudoku.NewGrid()

			data, err := ioutil.ReadFile(options.PUZZLE_TO_SOLVE)

			if err != nil {
				log.Fatalln("Read error for specified file:", err)
			}

			//TODO: shouldn't a load method have a way to say the string provided is invalid?
			options.CONVERTER.Load(grid, string(data))
		}

		if grid == nil {
			//No grid to do anything with.
			log.Fatalln("No grid loaded.")
		}

		//TODO: use of this option leads to a busy loop somewhere... Is it related to the generate-multiple-and-difficulty hang?

		var directions *sudoku.SolveDirections

		if options.WALKTHROUGH || options.PRINT_STATS {
			directions = grid.HumanSolution(nil)
			if len(directions.Steps) == 0 {
				//We couldn't solve it. Let's check and see if the puzzle is well formed.
				if grid.HasMultipleSolutions() {
					//TODO: figure out why guesses wouldn't be used here effectively.
					log.Println("The puzzle had multiple solutions; that means it's not well-formed")
				}
			}
		}

		if options.WALKTHROUGH {
			if options.OUTPUT_CSV {
				csvRec = append(csvRec, directions.Walkthrough())
			} else {
				fmt.Fprintln(output, directions.Walkthrough())
			}
		}
		if options.PRINT_STATS {
			if options.OUTPUT_CSV {
				csvRec = append(csvRec, strconv.FormatFloat(grid.Difficulty(), 'f', -1, 64))
				//We won't print out the directions.Stats() like we do for just printing to stdout,
				//because that's mostly noise in this format.
			} else {
				fmt.Fprintln(output, grid.Difficulty())
				//TODO: consider actually printing out the Signals stats (with a Stats method on signals)
				fmt.Fprintln(output, strings.Join(directions.Stats(), "\n"))
			}
		}
		if options.PUZZLE_TO_SOLVE != "" {
			grid.Solve()
			if options.OUTPUT_CSV {
				csvRec = append(csvRec, options.CONVERTER.DataString(grid))
			} else {
				fmt.Fprintln(output, options.CONVERTER.DataString(grid))

			}
		}

		if options.OUTPUT_CSV {
			csvWriter.Write(csvRec)
		}

		if options.PUZZLE_TO_SOLVE != "" {
			//If we're asked to solve, n could only be 1 anyway.
			return
		}
		grid.Done()
		if options.NUM > 1 {
			bar.Incr()
		}
	}
	if options.OUTPUT_CSV {
		csvWriter.Flush()
	}
}

func puzzleDirectoryParts(symmetryType sudoku.SymmetryType, symmetryPercentage float64, minFilledCells int) []string {
	return []string{
		STORED_PUZZLES_DIRECTORY,
		"SYM_TYPE_" + strconv.Itoa(int(symmetryType)),
		"SYM_PERCENTAGE_" + strconv.FormatFloat(symmetryPercentage, 'f', -1, 64),
		"MIN_FILED_CELLS_" + strconv.Itoa(minFilledCells),
	}
}

func storePuzzle(grid *sudoku.Grid, difficulty float64, symmetryType sudoku.SymmetryType, symmetryPercentage float64, minFilledCells int) bool {
	//TODO: we should include a hashed version of our difficulty weights file so we don't cache ones with old weights.
	directoryParts := puzzleDirectoryParts(symmetryType, symmetryPercentage, minFilledCells)

	fileNamePart := strconv.FormatFloat(difficulty, 'f', -1, 64) + ".sdk"

	pathSoFar := ""

	for i, part := range directoryParts {
		if i == 0 {
			pathSoFar = part
		} else {
			pathSoFar = filepath.Join(pathSoFar, part)
		}
		if _, err := os.Stat(pathSoFar); os.IsNotExist(err) {
			//need to create it.
			os.Mkdir(pathSoFar, 0700)
		}
	}

	fileName := filepath.Join(pathSoFar, fileNamePart)

	file, err := os.Create(fileName)

	if err != nil {
		log.Println(err)
		return false
	}

	defer file.Close()

	puzzleText := grid.DataString()

	n, err := io.WriteString(file, puzzleText)

	if err != nil {
		log.Println(err)
		return false
	} else {
		if n < len(puzzleText) {
			log.Println("Didn't write full file, only wrote", n, "bytes of", len(puzzleText))
			return false
		}
	}
	return true
}

func vendPuzzle(min float64, max float64, symmetryType sudoku.SymmetryType, symmetryPercentage float64, minFilledCells int) *sudoku.Grid {

	directory := filepath.Join(puzzleDirectoryParts(symmetryType, symmetryPercentage, minFilledCells)...)

	if files, err := ioutil.ReadDir(directory); os.IsNotExist(err) {
		//The directory doesn't exist.
		return nil
	} else {
		//OK, the directory exists, now see which puzzles are there and if any fit. If one does, vend it and delete the file.
		for _, file := range files {
			//See what this actually returns.
			filenameParts := strings.Split(file.Name(), ".")

			//Remember: there's a dot in the filename due to the float seperator.
			//TODO: shouldn't "sdk" be in a constant somewhere?
			if len(filenameParts) != 3 || filenameParts[2] != "sdk" {
				continue
			}

			difficulty, err := strconv.ParseFloat(strings.Join(filenameParts[0:2], "."), 64)
			if err != nil {
				continue
			}

			if min <= difficulty && difficulty <= max {
				//Found a puzzle!
				grid := sudoku.NewGrid()
				fullFileName := filepath.Join(directory, file.Name())
				grid.LoadFromFile(fullFileName)
				os.Remove(fullFileName)
				return grid
			}
		}
	}
	return nil
}

func generatePuzzle(min float64, max float64, symmetryType sudoku.SymmetryType, symmetryPercentage float64, minFilledCells int, skipCache bool) *sudoku.Grid {
	var result *sudoku.Grid

	if !skipCache {
		result = vendPuzzle(min, max, symmetryType, symmetryPercentage, minFilledCells)

		if result != nil {
			log.Println("Vending a puzzle from the cache.")
			return result
		}
	}

	options := sudoku.GenerationOptions{
		Symmetry:           symmetryType,
		SymmetryPercentage: symmetryPercentage,
		MinFilledCells:     minFilledCells,
	}

	//We'll have to generate one ourselves.
	count := 0
	for {
		//The first time we don't bother saying what number attemp it is, because if the first run is likely to generate a useable puzzle it's just noise.
		if count != 0 {
			log.Println("Attempt", count, "at generating puzzle.")
		}

		result = sudoku.GenerateGrid(&options)

		difficulty := result.Difficulty()

		if difficulty >= min && difficulty <= max {
			return result
		}

		log.Println("Rejecting grid of difficulty", difficulty)
		if storePuzzle(result, difficulty, symmetryType, symmetryPercentage, minFilledCells) {
			log.Println("Stored the puzzle for future use.")
		}

		count++
	}
	return nil
}
<|endoftext|>