// hc_cry.go : Attack based solely on the cryptogram, 
//             using George Lasry's algorithm.
//  author: Jean-François BOUCHAUDY
//  v0.9.8: current version - 20/06/2026
package main

import (
    "fmt"
    "bufio"
    "flag"
    "strings"
    "os"
    "os/signal"
    "syscall"
    "math"
    "math/rand"
    "strconv"
    "time"

    "m209/v3"
    "hc_score"
    "neighborsLugs"
    "neighborsPins/v2"
    "manual"
)
var PINS string
var LUGS [21]int
var thePinsTransformations []string   
var SA_ITER int
var TEMP_0 float64
var COOLING_FACTOR float64
var nod int
var DEBUG bool
var METHOD string
var mFile string
var maxScore float64
var maxPins [131]byte
var maxLugs [21]int
var maxPlain string
var sigs chan os.Signal
var theLugsUsed string
var zz int
var CandidateLugs [21]int
var START_TIME time.Time

// --- Monitor() : write the Lugs used to a file
func monitor() {
  for {
    sig := <-sigs
    if sig == syscall.SIGINT {
       pid := fmt.Sprintf("%d", os.Getpid())
       file, _ := os.OpenFile("lugs" + pid + ".txt", 
        os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600)
       _, err := file.WriteString(theLugsUsed) 
       if err != nil {
            fmt.Println("====Probleme")
       }
       file.Close() 
    }
  }
}

// --- Return the elapsed time 
func elapsedTime() string {
    seconds := time.Now().Unix() - START_TIME.Unix()
    if seconds < 60 { 
        return fmt.Sprintf("%5d s", seconds) 
    } else { 
        minutes := (seconds / 60)%60
        heures := seconds / 3600
        return fmt.Sprintf("%3d h %2d min", heures, minutes)
    }
}

func addLugs(tb [21]int) {
    theLugsUsed += fmt.Sprintf("%v\n", tb)
}

func lec_std(saisie string) string {
    ch1 := strings.ToUpper(saisie)
    ch2 := []byte{}
    for i := 0; i < len(ch1); i++ {
        if ch1[i] >= 'A' && ch1[i] <= 'Z' {
            ch2 = append(ch2, ch1[i])
        }
    }
    return string(ch2)
}

func initKeyPin() string {
    currentKeyPins := ""
    for i:=0; i< 131; i++ {
        if rand.Float64() > 0.5 {
            currentKeyPins += "1"
        } else {
            currentKeyPins += "0"
        }
    }
    return currentKeyPins
}
   
// -- SA_inner() : Inner Simulated Annealing 
//                 return BestPins
// -----------------------------------------
func SA_inner(
    e *m209.M209,
    crypto string,
) ([131]byte) {
    var bestPins [131]byte
    var currentPins, candidatePins [131]byte 
    var finish, skip bool
    var score1, score2 float64
    var plain, plain1, bestPlain string
    
    bestPins = manual.RandomPins(PINS)
    currentPins = bestPins
    Temp := TEMP_0

    e.SetAllPins(string(bestPins[:]))
    bestPlain = m209.Cipher(e,crypto)
    score2 = hc_score.SCORE[METHOD](bestPlain)

    for z := 0; z < SA_ITER; z++ {
        sa := 0; accepted := 0    
        for _, aTransform := range thePinsTransformations {
            finish = false
            for ! finish {
                candidatePins, finish, skip =
                    neighborsPins.GetCandidatePins[aTransform](currentPins)
                if skip {
                    continue
                }
                e.SetAllPins(string(candidatePins[:]))
                plain1 = m209.Cipher(e,crypto);nod++
                score1 = hc_score.SCORE[METHOD](plain1);nod++

                D := score1 - score2
                if D > 0 || rand.Float64() < math.Exp(-math.Abs(D)/Temp) {
                    currentPins = candidatePins
                    score2 = score1

                    e.SetAllPins(string(bestPins[:]));
                    plain = m209.Cipher(e,crypto)
                    score1 = hc_score.SCORE[METHOD](plain)
                    if score2 > score1 {
                        bestPins = candidatePins
                        bestPlain = plain1
                    }
                    if D < 0 { sa++ }; accepted ++
                } 
            }
        }
        Temp = Temp * COOLING_FACTOR
        if DEBUG {
            fmt.Printf("%2d %2d %8.6f %-20s ", zz, z, score2, bestPlain[:20])
            fmt.Printf(" sa/acc:%4d/%4d\n", sa, accepted)
        }
    }           
    return bestPins
}

func main() {
    var theLugsTransformations []string
    START_TIME = time.Now()

    // 1 - Manage Options (P,D,Z,i,S,T,A,B,L,z,F,M,m,R)
    // ------------------
    pPins := flag.String("P", "",
        "The Pins as a binary number of 131 digits 00...0 by default")
    dFlag := flag.Bool("D", false,  "Debug Mode")
    pLUGS_TRIES := flag.Int("Z", 1, "Shotgun for Outer")
    pSA_iter := flag.Int("i", 5, "Iterations for Inner (5 by default)")
    pThreshold := flag.Float64("T", 9999999999.0 , "Score Threshold ")
    pTransPins := flag.String("A", "TE,SI,SO,IW",
        "The Pins transformations (TE,SI,SO,IW,IA), ST skip Pins search")
    pTransLugs := flag.String("B", "TL",
          "TA,TL,ST: The Lugs transformations, ST skip Lugs search")
    pLugs := flag.String("L", "", "Array of 21 int")
    pT0 := flag.Float64("z", 0.003, "Temperature Zero")
    pF  := flag.Float64("F", 0.3, "Cooling Factor")
    pMethod := flag.String("M","UNI", "Fitness Method (UNI,IC,...)")
    pFileMethod := flag.String("m","m209", "File associated with Method")
    pReferenceManual := flag.String("R", "1", 
        "The Reference Manual (1942, ...),0: no restriction, 1:Only Lugs")
    pChance := flag.Int("C", 0, "Seed for pseudo Random generation")

    flag.Parse()

    DEBUG = *dFlag
    PINS = *pPins
    if *pPins != "" {
        PINS = *pPins
        manual.Flag_skip_SA = true
        if len(PINS) != 131 {
            fmt.Println("ERROR: you don't provide PINS")
            os.Exit(1)
        }
    } else { manual.Flag_skip_SA = false }
    mFile := *pFileMethod
    METHOD = *pMethod
    OUTER_TRIES := *pLUGS_TRIES
    SA_ITER = *pSA_iter
    THRESHOLD := *pThreshold
    TEMP_0 = *pT0
    COOLING_FACTOR = *pF
    tb := strings.Split(*pTransLugs, ",")
    theLugsTransformations = make([]string, len(tb))
    for i := 0; i < len(tb); i++ {
        theLugsTransformations[i] = tb[i]
    }
    tb = strings.Split(*pTransPins, ",")
    thePinsTransformations = make([]string, len(tb))
    for i:= 0; i < len(tb); i++ {
        thePinsTransformations[i] = tb[i]
    }
    if *pLugs != "" {
        tb := strings.Split(*pLugs, ":")
        theLugs := make([]int, 21)
        for i := 0; i < len(tb); i++ {
            x, _ := strconv.Atoi(tb[i])
            theLugs[i] = x
        }
        LUGS = ([21]int)(theLugs)
        manual.Flag_skip_HC = true
    } else { manual.Flag_skip_HC = false }      
    manual.REFERENCE_MANUAL = *pReferenceManual
    if *pChance != 0 {
        manual.InitRandom( *pChance )
    }

    if DEBUG {
        fmt.Println("Pins:", PINS)
        fmt.Println("Lugs:", LUGS)
        fmt.Println("Flag to skip HC:", manual.Flag_skip_HC )
        fmt.Println("Flag to skip SA:", manual.Flag_skip_SA )
        fmt.Println("Manual : ", manual.REFERENCE_MANUAL)
        fmt.Println("SA: T0,CF,iter:", TEMP_0, COOLING_FACTOR,SA_ITER)
        fmt.Println("Fitness: function,file, Thresold: ", 
            METHOD, mFile, THRESHOLD)
        fmt.Println("Shutgun [HC_outer]:", OUTER_TRIES)
        fmt.Println("Pins transformations:", *pTransPins)
        fmt.Println("Lugs transformations:", *pTransLugs)
        fmt.Println("Seed: ", *pChance)
    }
    
    // 2 - Reading the cryptogram from the standard input
    //---------------------------------------------------
    saisie := ""
    scanner := bufio.NewScanner(os.Stdin)
    for scanner.Scan() {
        // fmt.Println(scanner.Text())
        saisie += scanner.Text()
    }
    Crypto := lec_std(saisie)
    if DEBUG {
        fmt.Println("Message: \n", Crypto)
    }

    // 3 - Instantiate an M209 and initialize it
    //------------------------------------------
    e := m209.NewM209()
    hc_score.SCORE_INIT[METHOD](mFile)

    // 4 - HC_outer: a large loop that tests the neighboring 
    //     Lugs from a random configuration. The Pin search  
    //     is called for each Lug configuration (SA_inner()).
    //     HC_outer is invoked multiples times 
    //     (TRIES times: Shotgun approach).
    // ------------------------------------------------------
    fmt.Println("---SA_outer-----", 
        (time.Now().String())[:19], 
        " PID: ", os.Getpid(),
    )
    maxScore = 0
    maxPlain = strings.Repeat("A",len(Crypto))
    maxLugs  = [21]int{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}
    maxPins  = [131]byte{}
    sigs = make(chan os.Signal, 1)
    signal.Notify(sigs, syscall.SIGINT)
    go monitor()
    theLugsUsed = ""
    var flag_print_key bool = false

    for zz= 0 ; zz<OUTER_TRIES; zz++ {

        // 4.1 BestLugs and BestPins are initialized randomly.
        BestLugs := manual.RandomLugs(LUGS)
        BestPins := manual.RandomPins(PINS)
        e.SetAllPins(string(BestPins[:]))
        e.SetLugsOvers(BestLugs); addLugs(BestLugs)
        plain1 := m209.Cipher(&e, Crypto); nod++
        score1 := hc_score.SCORE[METHOD](plain1)
        BestPlain := plain1
        if DEBUG {
            fmt.Println("Init:", zz,BestLugs)
        }

        // 4.2 We keep repeating as long as there is an improvement.
        for {
            stuck := true

            // 4.2.1 We loop as long as there is a lug candidate.
            for _, aLugTransform := range theLugsTransformations {
                finish := false ; skip := false
                for ! finish {
                    CandidateLugs, finish, skip = 
                     neighborsLugs.GetCandidateLugs[aLugTransform](BestLugs) 
                    if skip { continue }
                    if ! manual.VerifyLugs(CandidateLugs) { continue }
                    e.SetLugsOvers(CandidateLugs); addLugs(CandidateLugs)
                    CandidatePins := SA_inner(&e, Crypto)

                    e.SetAllPins(string(CandidatePins[:]))
                    plain2 := m209.Cipher(&e,Crypto)
                    score2 := hc_score.SCORE[METHOD](plain2);nod++

                    Delta := score2 - score1
                    if Delta > 0 {
                        BestLugs = CandidateLugs
                        BestPins = CandidatePins
                        BestPlain = plain2
                        score1 = score2
                        stuck = false
                        if DEBUG {
                          fmt.Print("IMPROVED:")
                          fmt.Println(",",BestLugs, score1, string(BestPins[:])) 
                        }
                    }
                }
            }
            if stuck { break }
        }
        if score1 > maxScore { 
            maxScore = score1
            maxPlain = BestPlain
            maxLugs = BestLugs 
            maxPins = BestPins
            flag_print_key = true
        }
        if DEBUG || (score1 > THRESHOLD) || flag_print_key {
            fmt.Printf("Best Score: %6.4f ", score1)
            fmt.Printf("Best Plain: %-30s ", BestPlain[:30])
            fmt.Print("Best Lugs : ", BestLugs, " nod: ", nod)
            fmt.Print(" Best Pins : ", string(BestPins[:]))
            fmt.Print(" ", (time.Now().String())[:19]," ",zz)
            fmt.Println(elapsedTime())
            flag_print_key = false
        }
    }
    fmt.Println("----------- End ------------ ", (time.Now().String())[:19])
}

