summaryrefslogtreecommitdiff
path: root/elm/AdvSearch/Fields.elm
blob: ff7c20045205185d201b49f2e285bc8361f83175 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
module AdvSearch.Fields exposing (..)

import Html exposing (..)
import Html.Attributes exposing (..)
import Array
import Set
import Lib.Util exposing (..)
import Lib.Html exposing (..)
import Lib.DropDown as DD
import Lib.Api as Api
import Lib.Autocomplete as A
import AdvSearch.Anime as AA
import AdvSearch.Set as AS
import AdvSearch.Producers as AP
import AdvSearch.Staff as AT
import AdvSearch.Tags as AG
import AdvSearch.Traits as AI
import AdvSearch.RDate as AD
import AdvSearch.Range as AR
import AdvSearch.Resolution as AE
import AdvSearch.Engine as AEng
import AdvSearch.Birthday as AB
import AdvSearch.Lib exposing (..)


-- "Nested" fields are a container for other fields.
-- The code for nested fields is tightly coupled with the generic 'Field' abstraction below.

type alias NestModel =
  { ptype   : QType -- type of the parent field
  , qtype   : QType -- type of the child fields
  , fields  : List Field
  , and     : Bool
  , andDd   : DD.Config FieldMsg
  , addDd   : DD.Config FieldMsg
  , addtype : List QType
  , neg     : Bool -- only if ptype /= qtype
  }


type NestMsg
  = NAndToggle Bool
  | NAnd Bool Bool
  | NAddToggle Bool
  | NAdd Int
  | NAddType (List QType)
  | NField Int FieldMsg
  | NNeg Bool Bool


nestInit : Bool -> QType -> QType -> List Field -> Data -> (Data, NestModel)
nestInit and ptype qtype list dat =
  ( { dat | objid = dat.objid+2 }
  , { ptype   = ptype
    , qtype   = qtype
    , fields  = list
    , and     = and
    , andDd   = DD.init ("advsearch_field"++String.fromInt (dat.objid+0)) (FSNest << NAndToggle)
    , addDd   = DD.init ("advsearch_field"++String.fromInt (dat.objid+1)) (FSNest << NAddToggle)
    , addtype = [qtype]
    , neg     = False
    }
  )


nestUpdate : Data -> NestMsg -> NestModel -> (Data, NestModel, Cmd NestMsg)
nestUpdate dat msg model =
  case msg of
    NAndToggle b -> (dat, { model | andDd = DD.toggle model.andDd b, addtype = [model.qtype] }, Cmd.none)
    NAnd b _ -> (dat, { model | and = b, andDd = DD.toggle model.andDd False }, Cmd.none)
    NAddToggle b -> (dat, { model | addDd = DD.toggle model.addDd b, addtype = [model.qtype] }, Cmd.none)
    NAdd n ->
      let addPar lst (ndat,f) =
            case lst of
              (a::b::xs) ->
                -- Don't add the child field if it's an And/Or, the parent field covers that already.
                let nf = case f of
                          (_,_,FMNest m) -> if m.ptype == m.qtype then [] else [f]
                          _ -> [f]
                in addPar (b::xs) (nestInit True b a nf ndat |> Tuple.mapSecond FMNest |> fieldCreate -1)
              _ -> (ndat,f)
          (ndat2,f2) = addPar model.addtype (fieldInit n dat)
      in (ndat2, { model | addDd = DD.toggle model.addDd False, addtype = [model.qtype], fields = model.fields ++ [f2] }, Cmd.none)
    NAddType t -> (dat, { model | addtype = t }, Cmd.none)
    NField n FDel -> (dat, { model | fields = delidx n model.fields }, Cmd.none)
    NField n FMoveSub ->
      let subfields = List.drop n model.fields |> List.take 1 |> List.map (\(fid,fdd,fm) -> (fid, DD.toggle fdd False, fm))
          (ndat,subm) = nestInit (not model.and) model.qtype model.qtype subfields dat
          (ndat2,subf) = fieldCreate -1 (ndat, FMNest subm)
      in (ndat2, { model | fields = modidx n (always subf) model.fields }, Cmd.none)
    NField n m ->
      case List.head (List.drop n model.fields) of
        Nothing -> (dat, model, Cmd.none)
        Just f ->
          let (ndat, nf, nc) = fieldUpdate dat m f
          in (ndat, { model | fields = modidx n (always nf) model.fields }, Cmd.map (NField n) nc)
    NNeg b _ -> (dat, { model | neg = b }, Cmd.none)


nestToQuery : Data -> NestModel -> Maybe Query
nestToQuery dat model =
  let op  = if model.neg then Ne   else Eq
      com = if model.and then QAnd else QOr
      wrap f =
        case List.filterMap (fieldToQuery dat) model.fields of
          []  -> Nothing
          [x] -> Just (f x)
          xs  -> Just (f (com xs))
  in case (model.ptype, model.qtype) of
      (V,  R) -> wrap (QQuery 50 op)
      (V,  C) -> wrap (QQuery 51 op)
      (V,  S) -> wrap (QQuery 52 op)
      (C,  S) -> wrap (QQuery 52 op)
      (C,  V) -> wrap (QQuery 53 op)
      (R,  V) -> wrap (QQuery 53 op)
      _       -> wrap identity


nestFromQuery : QType -> QType -> Data -> Query -> Maybe (Data, NestModel)
nestFromQuery ptype qtype dat q =
  let init and l =
        let (ndat,fl) = List.foldr (\f (d,a) -> let (nd,fm) = fieldFromQuery qtype d f in (nd,(fm::a))) (dat,[]) l
        in nestInit and ptype qtype fl ndat

      initSub op val = if op /= Eq && op /= Ne then Nothing else Just <|
        let (ndat,f) = fieldFromQuery qtype dat val
            (ndat2,m) = nestInit True ptype qtype [f] ndat
            -- If there is only a single nested query and it's an and/or nest, merge it into this node.
            m2 = case m.fields of
                  [(_,_,FMNest cm)] -> if cm.ptype == cm.qtype then { m | fields = cm.fields, and = cm.and } else m
                  _ -> m
        in (ndat2, { m2 | neg = op == Ne })

  in case (ptype, qtype, q) of
       (V, R, QQuery 50 op r) -> initSub op r
       (V, C, QQuery 51 op r) -> initSub op r
       (V, S, QQuery 52 op r) -> initSub op r
       (C, S, QQuery 52 op r) -> initSub op r
       (C, V, QQuery 53 op r) -> initSub op r
       (R, V, QQuery 53 op r) -> initSub op r
       (_, _, QAnd l) -> if ptype == qtype then Just (init True  l) else Nothing
       (_, _, QOr  l) -> if ptype == qtype then Just (init False l) else Nothing
       _ -> Nothing


nestView : Data -> DD.Config FieldMsg -> NestModel -> Html FieldMsg
nestView dat dd model =
  let
    isNest (_,_,f) =
     case f of
       FMNest _ -> True
       _ -> False
    hasNest = List.any isNest model.fields
    filterDat =
      { dat
      | level = if model.ptype /= model.qtype then 1 else dat.level+1
      , parentTypes = if model.ptype /= model.qtype then Set.insert (showQType model.ptype) dat.parentTypes else dat.parentTypes
      }
    filters = List.indexedMap (\i f ->
        Html.map (FSNest << NField i) <| fieldView filterDat f
      ) model.fields

    add =
      let parents = Set.union filterDat.parentTypes <| Set.fromList <| List.map showQType <| List.drop 1 model.addtype
          lst = Array.toIndexedList fields |> List.filter (\(_,f) ->
                     Just f.ptype == List.head model.addtype
                  && f.title /= ""
                  && (dat.uid /= Nothing || f.title /= "My Labels")
                  && (dat.uid /= Nothing || f.title /= "My List")
                  && (f.title /= "Name" || not (Set.isEmpty parents))
                  && not (f.title == "Role" && (List.head (List.drop 1 model.addtype)) == Just C) -- No "role" filter for character seiyuu (the seiyuu role is implied, after all)
                  && not (Set.member (showQType f.qtype) parents))
          showT par t =
            case (par,t) of
              (_,V) -> "VN"
              (_,R) -> "Release"
              (_,C) -> "Character"
              (C,S) -> "VA"
              (_,S) -> "Staff"
          breads pre par l =
            case l of
              [] -> []
              [x] -> [ b [] [ text (showT par x) ] ]
              (x::xs) -> a [ href "#", onClickD (FSNest (NAddType (x::pre))) ] [ text (showT par x) ] :: text " » " :: breads (x::pre) x xs
      in
      div [ class "elm_dd_input elm_dd_noarrow short" ]
      [ DD.view model.addDd Api.Normal (text "+") <| \() ->
        [ div [ class "advheader", style "min-width" "200px" ]
          [ h3 [] [ text "Add filter" ]
          , if List.length model.addtype <= 1 then text "" else
            div [] <| breads [] model.qtype (List.reverse model.addtype)
          ]
        , ul (if List.length lst > 6 then [ style "columns" "2" ] else []) <|
              List.map (\(n,f) ->
                li [] [ a [ href "#", onClickD (FSNest <| if f.qtype /= f.ptype then NAddType (f.qtype :: model.addtype) else NAdd n)] [ text f.title ] ]
              ) lst
        ]
      ]

    andcont () = [ ul []
      [ li [] [ linkRadio (    model.and) (FSNest << NAnd True ) [ text "And: All filters must match" ] ]
      , li [] [ linkRadio (not model.and) (FSNest << NAnd False) [ text "Or: At least one filter must match"  ] ]
      ] ]

    andlbl = text <| if model.and then "And" else "Or"

    and = div [ class "elm_dd_input short" ] [ DD.view model.andDd Api.Normal andlbl andcont ]

    negcont () =
      let (a,b) =
            case (model.ptype, model.qtype) of
              (_, C) -> ("Has a character that matches these filters", "Does not have a character that matches these filters")
              (_, R) -> ("Has a release that matches these filters", "Does not have a release that matches these filters")
              (_, V) -> ("Linked to a visual novel that matches these filters", "Not linked to a visual novel that matches these filters")
              (V, S) -> ("Has staff that matches these filters", "Does not have staff that matches these filters")
              (C, S) -> ("Has a voice actor that matches these filters", "Does not have a voice actor that matches these filters")
              _ -> ("","")
      in [ ul []
        [ li [] [ linkRadio (not model.neg) (FSNest << NNeg False) [ text a ] ]
        , li [] [ linkRadio (    model.neg) (FSNest << NNeg True ) [ text b ] ]
        ] ]

    neglbl = text <| (if model.neg then "¬" else "") ++
      case (model.ptype, model.qtype) of
        (_, C) -> "Char"
        (_, R) -> "Rel"
        (_, V) -> "VN"
        (V, S) -> "Staff"
        (C, S) -> "VA"
        _ -> ""

    ourdd =
      if model.qtype == model.ptype
      then fieldViewDd dat dd andlbl andcont
      else fieldViewDd dat dd neglbl negcont

    initialdd = if model.ptype == model.qtype || List.length model.fields == 1 then [ ourdd ] else [ ourdd, and ]

  in
    if hasNest
    then table [ class "advnest" ] <| List.indexedMap (\i f -> tr []
          [ td [] <| if i == 0 then initialdd else []
          , td [ class (if i == 0 then "start" else "mid") ] [ div [] [], span [] [] ]
          , td [] [ f ]
          ]) filters
          ++ [ tr []
               [ td [] []
               , td [ class "end" ] [ div [] [], span [] [] ]
               , td [] [ add ]
               ]
             ]
    else table [ class "advrow" ] [ tr []
         [ td [] (initialdd ++ [b [ class "grayedout" ] [ text " → " ]])
         , td [] (filters ++ [add]) ] ]





-- Generic field abstraction.
-- (this is where typeclasses would have been *awesome*)
--
-- The following functions and definitions are only intended to provide field
-- listings and function dispatchers, if the implementation of anything in here
-- is longer than a single line, it should get its own definition near where
-- the rest of that field is defined.

type alias Field = (Int, DD.Config FieldMsg, FieldModel) -- The Int is the index into 'fields'

type alias ListModel =
  { val : Int
  , lst : List (Query, String)
  }

type FieldModel
  = FMCustom     Query -- A read-only placeholder for Query values that failed to parse into a Field
  | FMNest       NestModel
  | FMList       ListModel
  | FMLang       (AS.Model String)
  | FMOLang      (AS.Model String)
  | FMRPlatform  (AS.Model String)
  | FMVPlatform  (AS.Model String)
  | FMLength     (AS.Model Int)
  | FMRole       (AS.Model String)
  | FMBlood      (AS.Model String)
  | FMSex        (AS.SexModel)
  | FMGender     (AS.Model String)
  | FMMedium     (AS.Model String)
  | FMVoiced     (AS.Model Int)
  | FMAniEro     (AS.Model Int)
  | FMAniStory   (AS.Model Int)
  | FMRType      (AS.Model String)
  | FMLabel      (AS.Model Int)
  | FMRList      (AS.Model Int)
  | FMSRole      (AS.Model String)
  | FMHeight     (AR.Model Int)
  | FMWeight     (AR.Model Int)
  | FMBust       (AR.Model Int)
  | FMWaist      (AR.Model Int)
  | FMHips       (AR.Model Int)
  | FMCup        (AR.Model String)
  | FMAge        (AR.Model Int)
  | FMPopularity (AR.Model Int)
  | FMRating     (AR.Model Int)
  | FMVotecount  (AR.Model Int)
  | FMMinAge     (AR.Model Int)
  | FMDeveloper  AP.Model
  | FMProducer   AP.Model
  | FMStaff      AT.Model
  | FMAnime      AA.Model
  | FMRDate      AD.Model
  | FMResolution AE.Model
  | FMEngine     AEng.Model
  | FMTag        AG.Model
  | FMTrait      AI.Model
  | FMBirthday   AB.Model

type FieldMsg
  = FSCustom     () -- Not actually used at the moment
  | FSNest       NestMsg
  | FSList       Int
  | FSLang       (AS.Msg String)
  | FSOLang      (AS.Msg String)
  | FSRPlatform  (AS.Msg String)
  | FSVPlatform  (AS.Msg String)
  | FSLength     (AS.Msg Int)
  | FSRole       (AS.Msg String)
  | FSBlood      (AS.Msg String)
  | FSSex        AS.SexMsg
  | FSGender     (AS.Msg String)
  | FSMedium     (AS.Msg String)
  | FSVoiced     (AS.Msg Int)
  | FSAniEro     (AS.Msg Int)
  | FSAniStory   (AS.Msg Int)
  | FSRType      (AS.Msg String)
  | FSLabel      (AS.Msg Int)
  | FSRList      (AS.Msg Int)
  | FSSRole      (AS.Msg String)
  | FSHeight     AR.Msg
  | FSWeight     AR.Msg
  | FSBust       AR.Msg
  | FSWaist      AR.Msg
  | FSHips       AR.Msg
  | FSCup        AR.Msg
  | FSAge        AR.Msg
  | FSPopularity AR.Msg
  | FSRating     AR.Msg
  | FSVotecount  AR.Msg
  | FSMinAge     AR.Msg
  | FSDeveloper  AP.Msg
  | FSProducer   AP.Msg
  | FSStaff      AT.Msg
  | FSAnime      AA.Msg
  | FSRDate      AD.Msg
  | FSResolution AE.Msg
  | FSEngine     AEng.Msg
  | FSTag        AG.Msg
  | FSTrait      AI.Msg
  | FSBirthday   AB.Msg
  | FToggle Bool
  | FDel       -- intercepted in nestUpdate
  | FMoveSub   -- intercepted in nestUpdate
  | FMovePar

type alias FieldDesc =
  { qtype     : QType
  , ptype     : QType
  , title     : String                     -- How it's listed in the field selection menu.
  , quick     : Int                        -- Whether it should be included in the default set of fields (>0) ("quick mode") and in which order.
  , init      : Data -> (Data, FieldModel) -- How to initialize an empty field
  , fromQuery : Data -> Query -> Maybe (Data, FieldModel)  -- How to initialize the field from a query
  }


-- XXX: Should this be lazily initialized instead? May impact JS load time like this.
fields : Array.Array FieldDesc
fields =
  let f qtype title quick wrap init fromq =
        { qtype     = qtype
        , ptype     = qtype
        , title     = title
        , quick     = quick
        , init      = \d -> (Tuple.mapSecond wrap (init d))
        , fromQuery = \d q -> Maybe.map (Tuple.mapSecond wrap) (fromq d q)
        }
      -- List type queries are fully defined here for convenience
      l qtype title quick lst =
        f qtype title quick FMList (\d -> (d, { val = 0, lst = lst }))
          (\d q -> List.indexedMap (\i (k,v) -> (i,k,v)) lst |> List.filter (\(i,k,_) -> k == q) |> List.head |> Maybe.map (\(i,_,_) -> (d, { val = i, lst = lst })))
      -- Nested queries
      n ptype qtype title =
        { qtype     = qtype
        , ptype     = ptype
        , title     = title
        , quick     = 0
        , init      = nestInit True ptype qtype [] >> Tuple.mapSecond FMNest
        , fromQuery = \d -> nestFromQuery ptype qtype d >> Maybe.map (Tuple.mapSecond FMNest)
        }
  in Array.fromList
  -- IMPORTANT: This list is processed in reverse order when reading a Query
  -- into Fields, so "catch all" fields must be listed first. In particular,
  -- FMNest with qtype == ptype go before everything else.

  --  T TITLE            QUICK  WRAP          INIT                    FROM_QUERY
  [ n V V "And/Or"
  , n V R "Release »"
  , n V S "Staff »"
  , n V C "Character »"
  , f V "Language"           1  FMLang        AS.init                 AS.langFromQuery
  , f V "Original language"  2  FMOLang       AS.init                 AS.olangFromQuery
  , f V "Platform"           3  FMVPlatform   AS.init                 AS.platformFromQuery
  , f V "Tags"               4  FMTag         AG.init                 (AG.fromQuery -1)
  , f V ""                  -4  FMTag         AG.init                 (AG.fromQuery 0)
  , f V ""                  -4  FMTag         AG.init                 (AG.fromQuery 1)
  , f V ""                  -4  FMTag         AG.init                 (AG.fromQuery 2)
  , f V "My Labels"          0  FMLabel       AS.init                 AS.labelFromQuery
  , l V "My List"            0 [(QInt 65 Eq 1, "On my list"),         (QInt 65 Ne 1, "Not on my list")]
  , f V "Length"             0  FMLength      AS.init                 AS.lengthFromQuery
  , f V "Developer"          0  FMDeveloper   AP.init                 (AP.fromQuery False)
  , f V "Release date"       0  FMRDate       AD.init                 AD.fromQuery
  , f V "Popularity"         0  FMPopularity  AR.popularityInit       AR.popularityFromQuery
  , f V "Rating"             0  FMRating      AR.ratingInit           AR.ratingFromQuery
  , f V "Number of votes"    0  FMVotecount   AR.votecountInit        AR.votecountFromQuery
  , f V "Anime"              0  FMAnime       AA.init                 AA.fromQuery
  , l V "Has description"    0 [(QInt 61 Eq 1, "Has description"),    (QInt 61 Ne 1, "No description")]
  , l V "Has anime"          0 [(QInt 62 Eq 1, "Has anime relation"), (QInt 62 Ne 1, "No anime relation")]
  , l V "Has screenshot"     0 [(QInt 63 Eq 1, "Has screenshot(s)"),  (QInt 63 Ne 1, "No screenshot(s)")]
  , l V "Has review"         0 [(QInt 64 Eq 1, "Has review(s)"),      (QInt 64 Ne 1, "No review(s)")]

  , n R R "And/Or"
  , n R V "Visual Novel »"
  , f R "Language"           1  FMLang        AS.init                 AS.langFromQuery
  , f R "Platform"           2  FMRPlatform   AS.init                 AS.platformFromQuery
  , f R "Type"               3  FMRType       AS.init                 AS.rtypeFromQuery
  , l R "Patch"              0 [(QInt 61 Eq 1, "Patch to another release"),(QInt 61 Ne 1, "Standalone release")]
  , l R "Freeware"           0 [(QInt 62 Eq 1, "Freeware"),                (QInt 62 Ne 1, "Non-free")]
  , l R "Uncensored"         0 [(QInt 64 Eq 1, "Uncensored (no mosaic)"),  (QInt 64 Ne 1, "Censored (or no erotic content to censor)")]
  , l R "Official"           0 [(QInt 65 Eq 1, "Official"),                (QInt 65 Ne 1, "Unofficial")]
  , f R "Developer"          0  FMDeveloper   AP.init                 (AP.fromQuery False)
  , f R "Producer"           0  FMProducer    AP.init                 (AP.fromQuery True)
  , f R "Release date"       0  FMRDate       AD.init                 AD.fromQuery
  , f R "Resolution"         0  FMResolution  AE.init                 AE.fromQuery
  , f R "Age rating"         0  FMMinAge      AR.minageInit           AR.minageFromQuery
  , f R "Medium"             0  FMMedium      AS.init                 AS.mediumFromQuery
  , f R "Voiced"             0  FMVoiced      AS.init                 AS.voicedFromQuery
  , f R "Ero animation"      0  FMAniEro      AS.init                 (AS.animatedFromQuery False)
  , f R "Story animation"    0  FMAniStory    AS.init                 (AS.animatedFromQuery True)
  , f R "Engine"             0  FMEngine      AEng.init               AEng.fromQuery
  , f R "My List"            0  FMRList       AS.init                 AS.rlistFromQuery

  , n C C "And/Or"
  , n C S "Voice Actor »"
  , n C V "Visual Novel »"
  , f C "Role"               1  FMRole        AS.init                 AS.roleFromQuery
  , f C "Age"                0  FMAge         AR.ageInit              AR.ageFromQuery
  , f C "Birthday"           0  FMBirthday    AB.init                 AB.fromQuery
  , f C "Sex"                2  FMSex         (AS.sexInit False)      (AS.sexFromQuery False)
  , f C ""                   0  FMSex         (AS.sexInit True)       (AS.sexFromQuery True)
  , f C "Traits"             3  FMTrait       AI.init                 (AI.fromQuery -1)
  , f C ""                   0  FMTrait       AI.init                 (AI.fromQuery 0)
  , f C ""                   0  FMTrait       AI.init                 (AI.fromQuery 1)
  , f C ""                   0  FMTrait       AI.init                 (AI.fromQuery 2)
  , f C "Blood type"         0  FMBlood       AS.init                 AS.bloodFromQuery
  , f C "Height"             0  FMHeight      AR.heightInit           AR.heightFromQuery
  , f C "Weight"             0  FMWeight      AR.weightInit           AR.weightFromQuery
  , f C "Bust"               0  FMBust        AR.bustInit             AR.bustFromQuery
  , f C "Waist"              0  FMWaist       AR.waistInit            AR.waistFromQuery
  , f C "Hips"               0  FMHips        AR.hipsInit             AR.hipsFromQuery
  , f C "Cup size"           0  FMCup         AR.cupInit              AR.cupFromQuery

  , n S S "And/Or"
  , f S "Name"               0  FMStaff       AT.init                 AT.fromQuery
  , f S "Language"           1  FMLang        AS.init                 AS.langFromQuery
  , f S "Gender"             2  FMGender      AS.init                 AS.genderFromQuery
  , f S "Role"               3  FMSRole       AS.init                 AS.sroleFromQuery
  ]


fieldUpdate : Data -> FieldMsg -> Field -> (Data, Field, Cmd FieldMsg)
fieldUpdate dat msg_ (num, dd, model) =
  let maps f m = (dat, (num, dd, f m), Cmd.none)              -- Simple version: update function returns a Model
      mapf fm fc (d,m,c) = (d, (num, dd, fm m), Cmd.map fc c) -- Full version: update function returns (Data, Model, Cmd)
      mapc fm fc (d,m,c) = (d, (num, DD.toggle dd False, fm m), Cmd.map fc c) -- Full version that also closes the DD (Ugly hack...)
      noop = (dat, (num, dd, model), Cmd.none)

      -- Called when opening a dropdown, can be used to focus an input element
      focus =
        case model of
          FMTag        m -> Cmd.map FSTag        (A.refocus m.conf)
          FMTrait      m -> Cmd.map FSTrait      (A.refocus m.conf)
          FMDeveloper  m -> Cmd.map FSDeveloper  (A.refocus m.conf)
          FMProducer   m -> Cmd.map FSProducer   (A.refocus m.conf)
          FMStaff      m -> Cmd.map FSStaff      (A.refocus m.conf)
          FMAnime      m -> Cmd.map FSAnime      (A.refocus m.conf)
          FMResolution m -> Cmd.map FSResolution (A.refocus m.conf)
          FMEngine     m -> Cmd.map FSEngine     (A.refocus m.conf)
          _ -> Cmd.none
  in case (msg_, model) of
      -- Move to parent node is tricky, needs to be intercepted at this point so that we can access the parent NestModel.
      (FSNest (NField parentNum (FSNest (NField fieldNum FMovePar))), FMNest grandModel) ->
        case List.head <| List.drop parentNum grandModel.fields of
          Just (_,_,FMNest parentModel) ->
            let fieldField = List.drop fieldNum parentModel.fields |> List.take 1
                newFields = List.map (\(fid,fdd,fm) -> (fid, DD.toggle fdd False, fm)) fieldField
                newParentModel = { parentModel | fields = delidx fieldNum parentModel.fields }
                addGrandFields = List.take parentNum grandModel.fields ++ newFields ++ List.drop parentNum grandModel.fields
                newGrandFields =
                  if List.isEmpty newParentModel.fields
                  then delidx (parentNum+1) addGrandFields
                  else modidx (parentNum+1) (\(pid,pdd,_) -> (pid,pdd,FMNest newParentModel)) addGrandFields
                newGrandModel = { grandModel | fields = newGrandFields }
            in (dat, (num,dd,FMNest newGrandModel), Cmd.none)
          _ -> noop

      -- Move root node to sub; for child nodes this is handled in nestUpdate, but the root node must be handled separately
      (FMoveSub, FMNest m) ->
        let subfields = [(num,DD.toggle dd False,model)]
            (ndat,subm) = nestInit True m.qtype m.qtype subfields dat
            (ndat2,subf) = fieldCreate -1 (ndat, FMNest subm)
        in (ndat2, subf, Cmd.none)

      (FSNest (NAnd a b), FMNest m)  -> mapc FMNest FSNest (nestUpdate dat (NAnd a b) m)
      (FSNest (NNeg a b), FMNest m)  -> mapc FMNest FSNest (nestUpdate dat (NNeg a b) m)
      (FSNest msg,     FMNest m)     -> mapf FMNest FSNest (nestUpdate dat msg m)
      (FSList msg,     FMList m)     -> (dat, (num,DD.toggle dd False,FMList { m | val = msg }), Cmd.none)
      (FSLang msg,     FMLang m)     -> maps FMLang     (AS.update msg m)
      (FSOLang msg,    FMOLang m)    -> maps FMOLang    (AS.update msg m)
      (FSRPlatform msg,FMRPlatform m)-> maps FMRPlatform(AS.update msg m)
      (FSVPlatform msg,FMVPlatform m)-> maps FMVPlatform(AS.update msg m)
      (FSLength msg,   FMLength m)   -> maps FMLength   (AS.update msg m)
      (FSRole msg,     FMRole m)     -> maps FMRole     (AS.update msg m)
      (FSBlood msg,    FMBlood m)    -> maps FMBlood    (AS.update msg m)
      (FSSex msg,      FMSex m)      -> maps FMSex      (AS.sexUpdate msg m)
      (FSGender msg,   FMGender m)   -> maps FMGender   (AS.update msg m)
      (FSMedium msg,   FMMedium m)   -> maps FMMedium   (AS.update msg m)
      (FSVoiced msg,   FMVoiced m)   -> maps FMVoiced   (AS.update msg m)
      (FSAniEro msg,   FMAniEro m)   -> maps FMAniEro   (AS.update msg m)
      (FSAniStory msg, FMAniStory m) -> maps FMAniStory (AS.update msg m)
      (FSRType  msg,   FMRType m)    -> maps FMRType    (AS.update msg m)
      (FSLabel  msg,   FMLabel m)    -> maps FMLabel    (AS.update msg m)
      (FSRList  msg,   FMRList m)    -> maps FMRList    (AS.update msg m)
      (FSSRole  msg,   FMSRole m)    -> maps FMSRole    (AS.update msg m)
      (FSHeight msg,   FMHeight m)   -> maps FMHeight   (AR.update msg m)
      (FSWeight msg,   FMWeight m)   -> maps FMWeight   (AR.update msg m)
      (FSBust msg,     FMBust m)     -> maps FMBust     (AR.update msg m)
      (FSWaist msg,    FMWaist m)    -> maps FMWaist    (AR.update msg m)
      (FSHips msg,     FMHips m)     -> maps FMHips     (AR.update msg m)
      (FSCup msg,      FMCup m)      -> maps FMCup      (AR.update msg m)
      (FSAge msg,      FMAge m)      -> maps FMAge      (AR.update msg m)
      (FSPopularity msg,FMPopularity m)->maps FMPopularity (AR.update msg m)
      (FSRating msg,   FMRating m)   -> maps FMRating    (AR.update msg m)
      (FSVotecount msg,FMVotecount m)-> maps FMVotecount (AR.update msg m)
      (FSMinAge msg   ,FMMinAge m)   -> maps FMMinAge   (AR.update msg m)
      (FSDeveloper msg,FMDeveloper m)-> mapf FMDeveloper FSDeveloper (AP.update dat msg m)
      (FSProducer msg, FMProducer m) -> mapf FMProducer  FSProducer  (AP.update dat msg m)
      (FSStaff msg,    FMStaff m)    -> mapf FMStaff     FSStaff     (AT.update dat msg m)
      (FSAnime msg,    FMAnime m)    -> mapf FMAnime     FSAnime     (AA.update dat msg m)
      (FSRDate msg,    FMRDate m)    -> maps FMRDate    (AD.update msg m)
      (FSResolution msg,FMResolution m)->mapf FMResolution FSResolution (AE.update dat msg m)
      (FSEngine msg,   FMEngine m)   -> mapf FMEngine FSEngine (AEng.update dat msg m)
      (FSTag msg,      FMTag m)      -> mapf FMTag FSTag     (AG.update dat msg m)
      (FSTrait msg,    FMTrait m)    -> mapf FMTrait FSTrait (AI.update dat msg m)
      (FSBirthday msg, FMBirthday m) -> maps FMBirthday (AB.update msg m)
      (FToggle b, _) -> (dat, (num, DD.toggle dd b, model), if b then focus else Cmd.none)
      _ -> noop


fieldViewDd : Data -> DD.Config FieldMsg -> Html FieldMsg -> (() -> List (Html FieldMsg)) -> Html FieldMsg
fieldViewDd dat dd lbl cont =
  div [ class "elm_dd_input" ]
  [ DD.view dd Api.Normal lbl <| \() ->
      div [ class "advbut" ]
      [ if dat.level == 0
        then b [ title "Can't delete the top-level filter" ] [ text "⊗" ]
        else a [ href "#", onClickD FDel, title "Delete this filter" ] [ text "⊗" ]
      , if dat.level <= 1
        then b [ title "Can't move this filter to parent branch" ] [ text "↰" ]
        else a [ href "#", onClickD FMovePar, title "Move this filter to parent branch" ] [ text "↰" ]
      , a [ href "#", onClickD FMoveSub, title "Create new branch for this filter" ] [ text "↳" ]
      ] :: cont ()
  ]

fieldView : Data -> Field -> Html FieldMsg
fieldView dat (_, dd, model) =
  let f wrap (lbl,cont) = fieldViewDd dat dd (Html.map wrap lbl) <| \() -> List.map (Html.map wrap) (cont ())
      l m = ( span [ class "nowrap" ] [ text <| Maybe.withDefault "" <| Maybe.map Tuple.second <| List.head <| List.drop m.val m.lst ]
            , \() -> [ ul [] <| List.indexedMap (\n (_,v) -> li [] [ linkRadio (n == m.val) (\_ -> n) [ text v ] ]) m.lst ]
            )
  in case model of
      FMCustom m     -> f FSCustom     (text "Unrecognized query", \() -> [text ""]) -- TODO: Display the Query
      FMList m       -> f FSList       (l m)
      FMLang  m      -> f FSLang       (AS.langView False m)
      FMOLang m      -> f FSOLang      (AS.langView True  m)
      FMVPlatform m  -> f FSVPlatform  (AS.platformView False m)
      FMRPlatform m  -> f FSRPlatform  (AS.platformView True m)
      FMLength m     -> f FSLength     (AS.lengthView m)
      FMRole m       -> f FSRole       (AS.roleView m)
      FMBlood m      -> f FSBlood      (AS.bloodView m)
      FMSex m        -> f FSSex        (AS.sexView m)
      FMGender m     -> f FSGender     (AS.genderView m)
      FMMedium m     -> f FSMedium     (AS.mediumView m)
      FMVoiced m     -> f FSVoiced     (AS.voicedView m)
      FMAniEro m     -> f FSAniEro     (AS.animatedView False m)
      FMAniStory m   -> f FSAniStory   (AS.animatedView True m)
      FMRType m      -> f FSRType      (AS.rtypeView m)
      FMLabel m      -> f FSLabel      (AS.labelView dat m)
      FMRList m      -> f FSRList      (AS.rlistView m)
      FMSRole m      -> f FSSRole      (AS.sroleView m)
      FMHeight m     -> f FSHeight     (AR.heightView m)
      FMWeight m     -> f FSWeight     (AR.weightView m)
      FMBust m       -> f FSBust       (AR.bustView m)
      FMWaist m      -> f FSWaist      (AR.waistView m)
      FMHips m       -> f FSHips       (AR.hipsView m)
      FMCup m        -> f FSCup        (AR.cupView m)
      FMAge m        -> f FSAge        (AR.ageView m)
      FMPopularity m -> f FSPopularity (AR.popularityView m)
      FMRating m     -> f FSRating     (AR.ratingView m)
      FMVotecount m  -> f FSVotecount  (AR.votecountView m)
      FMMinAge m     -> f FSMinAge     (AR.minageView m)
      FMDeveloper m  -> f FSDeveloper  (AP.view False dat m)
      FMProducer m   -> f FSProducer   (AP.view True dat m)
      FMStaff m      -> f FSStaff      (AT.view dat m)
      FMAnime m      -> f FSAnime      (AA.view dat m)
      FMRDate m      -> f FSRDate      (AD.view m)
      FMResolution m -> f FSResolution (AE.view m)
      FMEngine m     -> f FSEngine     (AEng.view m)
      FMTag m        -> f FSTag        (AG.view dat m)
      FMTrait m      -> f FSTrait      (AI.view dat m)
      FMBirthday m   -> f FSBirthday   (AB.view m)
      FMNest m       -> nestView dat dd m


fieldToQuery : Data -> Field -> Maybe Query
fieldToQuery dat (_, _, model) =
  case model of
    FMCustom m   -> Just m
    FMList m     -> List.drop m.val m.lst |> List.head |> Maybe.map Tuple.first
    FMNest m     -> nestToQuery dat m
    FMLang  m    -> AS.toQuery (QStr 2) m
    FMOLang m    -> AS.toQuery (QStr 3) m
    FMRPlatform m-> AS.toQuery (QStr 4) m
    FMVPlatform m-> AS.toQuery (QStr 4) m
    FMLength m   -> AS.toQuery (QInt 5) m
    FMRole m     -> AS.toQuery (QStr 2) m
    FMBlood m    -> AS.toQuery (QStr 3) m
    FMSex (s,m)  -> AS.toQuery (QStr (if s then 5 else 4)) m
    FMGender m   -> AS.toQuery (QStr 4) m
    FMMedium m   -> AS.toQuery (QStr 11) m
    FMVoiced m   -> AS.toQuery (QInt 12) m
    FMAniEro m   -> AS.toQuery (QInt 13) m
    FMAniStory m -> AS.toQuery (QInt 14) m
    FMRType m    -> AS.toQuery (QStr 16) m
    FMLabel m    -> AS.toQuery (\op v -> QTuple 12 op (Maybe.withDefault 0 (Maybe.map vndbidNum dat.uid)) v) m
    FMRList m    -> AS.toQuery (QInt 18) m
    FMSRole m    -> AS.toQuery (QStr 5) m
    FMHeight m   -> AR.toQuery (QInt 6) (QStr 6) m
    FMWeight m   -> AR.toQuery (QInt 7) (QStr 7) m
    FMBust m     -> AR.toQuery (QInt 8) (QStr 8) m
    FMWaist m    -> AR.toQuery (QInt 9) (QStr 9) m
    FMHips m     -> AR.toQuery (QInt 10) (QStr 10) m
    FMCup m      -> AR.toQuery (QStr 11) (QStr 11) m
    FMAge m      -> AR.toQuery (QInt 12) (QStr 12) m
    FMPopularity m->AR.toQuery (QInt 9) (QStr 9) m
    FMRating m   -> AR.toQuery (QInt 10) (QStr 10) m
    FMVotecount m-> AR.toQuery (QInt 11) (QStr 11) m
    FMMinAge m   -> AR.toQuery (QInt 10) (QStr 10) m
    FMDeveloper m-> AP.toQuery False m
    FMProducer m -> AP.toQuery True m
    FMStaff m    -> AT.toQuery m
    FMAnime m    -> AA.toQuery m
    FMRDate m    -> AD.toQuery m
    FMResolution m-> AE.toQuery m
    FMEngine m   -> AEng.toQuery m
    FMTag m      -> AG.toQuery m
    FMTrait m    -> AI.toQuery m
    FMBirthday m -> AB.toQuery m


fieldCreate : Int -> (Data,FieldModel) -> (Data,Field)
fieldCreate fid (dat,fm) =
   ( {dat | objid = dat.objid + 1}
   , (fid, DD.init ("advsearch_field" ++ String.fromInt dat.objid) FToggle, fm)
   )


fieldInit : Int -> Data -> (Data,Field)
fieldInit n dat =
  case Array.get n fields of
    Just f -> fieldCreate n (f.init dat)
    Nothing -> fieldCreate -1 (dat, FMCustom (QAnd [])) -- Shouldn't happen.


fieldFromQuery : QType -> Data -> Query -> (Data,Field)
fieldFromQuery qtype dat q =
  let (field, _) =
        Array.foldr (\f (af,n) ->
          case (if af /= Nothing || f.ptype /= qtype then Nothing else f.fromQuery dat q) of
            Nothing -> (af,n-1)
            Just ret -> (Just (fieldCreate n ret), 0)
        ) (Nothing,Array.length fields-1) fields
  in case field of
      Just ret -> ret
      Nothing -> fieldCreate -1 (dat, FMCustom q)


fieldSub : Field -> Sub FieldMsg
fieldSub (_,dd,fm) =
  case fm of
    FMNest m ->
      Sub.batch
        <| DD.sub dd
        :: DD.sub m.addDd
        :: DD.sub m.andDd
        :: List.indexedMap (\i -> Sub.map (FSNest << NField i) << fieldSub) m.fields
    _ -> DD.sub dd