package tools
import (
"fmt"
"math"
"os"
"strings"
"skraak/utils"
)
type CallsModifyInput struct {
File string `json:"file"`
Reviewer string `json:"reviewer"`
Filter string `json:"filter"`
Segment string `json:"segment"`
Certainty int `json:"certainty"`
Species string `json:"species"`
Bookmark *bool `json:"bookmark"`
Comment string `json:"comment"`
}
type CallsModifyOutput struct {
File string `json:"file"`
SegmentStart int `json:"segment_start"`
SegmentEnd int `json:"segment_end"`
Species string `json:"species,omitempty"`
CallType string `json:"calltype,omitempty"`
Certainty int `json:"certainty,omitempty"`
Bookmark *bool `json:"bookmark,omitempty"`
Comment string `json:"comment,omitempty"`
PreviousValue string `json:"previous_value,omitempty"`
Error string `json:"error,omitempty"`
}
func validateModifyInput(input CallsModifyInput) error {
if input.File == "" {
return fmt.Errorf("--file is required")
}
if input.Reviewer == "" {
return fmt.Errorf("--reviewer is required")
}
if input.Filter == "" {
return fmt.Errorf("--filter is required")
}
if input.Segment == "" {
return fmt.Errorf("--segment is required")
}
if len(input.Comment) > 140 {
return fmt.Errorf("--comment must be 140 characters or less")
}
for i, r := range input.Comment {
if r > 127 {
return fmt.Errorf("--comment must be ASCII only (non-ASCII at position %d)", i)
}
}
return nil
}
func resolveSpecies(inputSpecies string, label *utils.Label) (species, callType string) {
if inputSpecies == "" {
return label.Species, label.CallType
}
if before, after, ok := strings.Cut(inputSpecies, "+"); ok {
return before, after
}
return inputSpecies, ""
}
func hasModifyChanges(newSpecies, newCallType string, input CallsModifyInput, label *utils.Label) bool {
if newSpecies != label.Species || newCallType != label.CallType {
return true
}
if input.Certainty != label.Certainty {
return true
}
if input.Bookmark != nil && *input.Bookmark != label.Bookmark {
return true
}
if input.Comment != "" {
return true
}
return false
}
func applyLabelChanges(label *utils.Label, dataFile *utils.DataFile, input CallsModifyInput, newSpecies, newCallType string, output *CallsModifyOutput) error {
dataFile.Meta.Reviewer = input.Reviewer
label.Species = newSpecies
label.CallType = newCallType
output.Species = newSpecies
output.CallType = newCallType
label.Certainty = input.Certainty
output.Certainty = input.Certainty
if input.Bookmark != nil && *input.Bookmark != label.Bookmark {
label.Bookmark = *input.Bookmark
output.Bookmark = input.Bookmark
}
if input.Comment != "" {
var newComment string
if label.Comment != "" {
newComment = label.Comment + " | " + input.Comment
} else {
newComment = input.Comment
}
if len(newComment) > 140 {
return fmt.Errorf("combined comment exceeds 140 characters (%d)", len(newComment))
}
label.Comment = newComment
output.Comment = newComment
}
return nil
}
func CallsModify(input CallsModifyInput) (CallsModifyOutput, error) {
var output CallsModifyOutput
if err := validateModifyInput(input); err != nil {
output.Error = err.Error()
return output, err
}
startTime, endTime, err := parseSegmentRange(input.Segment)
if err != nil {
output.Error = err.Error()
return output, err
}
output.File = input.File
output.SegmentStart = startTime
output.SegmentEnd = endTime
if _, err := os.Stat(input.File); os.IsNotExist(err) {
output.Error = fmt.Sprintf("File not found: %s", input.File)
return output, fmt.Errorf("%s", output.Error)
}
dataFile, err := utils.ParseDataFile(input.File)
if err != nil {
output.Error = fmt.Sprintf("Failed to parse file: %v", err)
return output, fmt.Errorf("%s", output.Error)
}
segment := findSegment(dataFile.Segments, startTime, endTime, input.Filter)
if segment == nil {
output.Error = fmt.Sprintf("No segment found matching time range %d-%d", startTime, endTime)
return output, fmt.Errorf("%s", output.Error)
}
targetLabel := findLabelByFilter(segment, input.Filter)
if targetLabel == nil {
output.Error = fmt.Sprintf("No label found with filter '%s' in segment %d-%d", input.Filter, startTime, endTime)
return output, fmt.Errorf("%s", output.Error)
}
output.PreviousValue = formatLabel(targetLabel)
newSpecies, newCallType := resolveSpecies(input.Species, targetLabel)
if !hasModifyChanges(newSpecies, newCallType, input, targetLabel) {
output.Error = "No changes needed: all values already match"
return output, fmt.Errorf("%s", output.Error)
}
if err := applyLabelChanges(targetLabel, dataFile, input, newSpecies, newCallType, &output); err != nil {
output.Error = err.Error()
return output, err
}
if err := dataFile.Write(input.File); err != nil {
output.Error = fmt.Sprintf("Failed to save file: %v", err)
return output, fmt.Errorf("%s", output.Error)
}
return output, nil
}
func findLabelByFilter(segment *utils.Segment, filter string) *utils.Label {
for _, label := range segment.Labels {
if label.Filter == filter {
return label
}
}
return nil
}
func parseSegmentRange(s string) (int, int, error) {
parts := strings.Split(s, "-")
if len(parts) != 2 {
return 0, 0, fmt.Errorf("invalid segment format: %s (expected start-end, e.g., 12-15)", s)
}
var start, end int
if _, err := fmt.Sscanf(parts[0], "%d", &start); err != nil {
return 0, 0, fmt.Errorf("invalid start time: %s", parts[0])
}
if _, err := fmt.Sscanf(parts[1], "%d", &end); err != nil {
return 0, 0, fmt.Errorf("invalid end time: %s", parts[1])
}
if start < 0 || end < 0 {
return 0, 0, fmt.Errorf("times must be non-negative")
}
if start >= end {
return 0, 0, fmt.Errorf("start time must be less than end time")
}
return start, end, nil
}
func findSegment(segments []*utils.Segment, startTime, endTime int, filter string) *utils.Segment {
for _, seg := range segments {
segStart := int(math.Floor(seg.StartTime))
segEnd := int(math.Ceil(seg.EndTime))
if segEnd == segStart {
segEnd = segStart + 1 }
if segStart == startTime && segEnd == endTime {
for _, label := range seg.Labels {
if label.Filter == filter {
return seg
}
}
}
}
return nil
}
func formatLabel(label *utils.Label) string {
result := label.Species
if label.CallType != "" {
result += "+" + label.CallType
}
result += fmt.Sprintf(" (%d%%)", label.Certainty)
return result
}