001/*
002 * This file is part of McIDAS-V
003 *
004 * Copyright 2007-2024
005 * Space Science and Engineering Center (SSEC)
006 * University of Wisconsin - Madison
007 * 1225 W. Dayton Street, Madison, WI 53706, USA
008 * https://www.ssec.wisc.edu/mcidas/
009 * 
010 * All Rights Reserved
011 * 
012 * McIDAS-V is built on Unidata's IDV and SSEC's VisAD libraries, and
013 * some McIDAS-V source code is based on IDV and VisAD source code.  
014 * 
015 * McIDAS-V is free software; you can redistribute it and/or modify
016 * it under the terms of the GNU Lesser Public License as published by
017 * the Free Software Foundation; either version 3 of the License, or
018 * (at your option) any later version.
019 * 
020 * McIDAS-V is distributed in the hope that it will be useful,
021 * but WITHOUT ANY WARRANTY; without even the implied warranty of
022 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
023 * GNU Lesser Public License for more details.
024 * 
025 * You should have received a copy of the GNU Lesser Public License
026 * along with this program.  If not, see https://www.gnu.org/licenses/.
027 */
028
029package edu.wisc.ssec.mcidasv.startupmanager.options;
030
031import java.io.BufferedReader;
032import java.io.BufferedWriter;
033import java.io.File;
034import java.io.FileNotFoundException;
035import java.io.FileReader;
036import java.io.FileWriter;
037import java.io.IOException;
038import java.util.ArrayList;
039import java.util.Collection;
040import java.util.Collections;
041import java.util.HashMap;
042import java.util.List;
043import java.util.Map;
044import java.util.stream.Collectors;
045
046import edu.wisc.ssec.mcidasv.startupmanager.StartupManager;
047import edu.wisc.ssec.mcidasv.startupmanager.Platform;
048import org.slf4j.Logger;
049import org.slf4j.LoggerFactory;
050
051public class OptionMaster {
052    
053    private static final Logger logger =
054        LoggerFactory.getLogger(OptionMaster.class);
055    
056    public final static String SET_PREFIX = "SET ";
057    public final static String EMPTY_STRING = "";
058    public final static String QUOTE_STRING = "\"";
059    public final static char QUOTE_CHAR = '"';
060
061    // TODO(jon): write CollectionHelpers.zip() and CollectionHelpers.zipWith()
062    public final Object[][] blahblah = {
063        // Default memory initial setting is 80% of system memory
064        { "HEAP_SIZE", "Memory", "80P", Type.MEMORY, OptionPlatform.ALL, Visibility.VISIBLE },
065        { "JOGL_TOGL", "Enable JOGL", "1", Type.BOOLEAN, OptionPlatform.UNIXLIKE, Visibility.VISIBLE },
066        { "USE_3DSTUFF", "Enable 3D controls", "1", Type.BOOLEAN, OptionPlatform.ALL, Visibility.VISIBLE },
067        { "DEFAULT_LAYOUT", "Load default layout", "1", Type.BOOLEAN, OptionPlatform.ALL, Visibility.VISIBLE },
068        { "STARTUP_BUNDLE", "Defaults", "0;", Type.FILE, OptionPlatform.ALL, Visibility.VISIBLE },
069        // mcidasv enables this (the actual property is "visad.java3d.geometryByRef")
070        // by default in mcidasv.properties.
071        { "USE_GEOBYREF", "Enable access to geometry by reference", "1", Type.BOOLEAN, OptionPlatform.ALL, Visibility.VISIBLE },
072        { "USE_IMAGEBYREF", "Enable access to image data by reference", "1", Type.BOOLEAN, OptionPlatform.ALL, Visibility.VISIBLE },
073        { "USE_NPOT", "Enable Non-Power of Two (NPOT) textures", "0", Type.BOOLEAN, OptionPlatform.ALL, Visibility.VISIBLE },
074        // USE_CMSGC is no longer in use, so the visibility is "HIDDEN".
075        // If we remove the option entirely, existing users with USE_CMSGC will may see
076        // a warning message.
077        { "USE_CMSGC", "Enable concurrent mark-sweep garbage collector", "0", Type.BOOLEAN, OptionPlatform.ALL, Visibility.HIDDEN },
078        { "LOG_LEVEL", "Log Level", "INFO", Type.LOGLEVEL, OptionPlatform.ALL, Visibility.VISIBLE },
079        { "JVM_OPTIONS", "Java Virtual Machine Options", "", Type.TEXT, OptionPlatform.ALL, Visibility.VISIBLE },
080        { "TEXTURE_WIDTH", "Texture Size", "4096", Type.TEXT, OptionPlatform.ALL, Visibility.VISIBLE },
081        { "MCV_SCALING", "GUI Scaling", "1", Type.TEXT, OptionPlatform.ALL, Visibility.VISIBLE },
082//        { "USE_DARK_MODE", "Enable Dark Mode", "0", Type.BOOLEAN, OptionPlatform.UNIXLIKE, Visibility.VISIBLE },
083        { "USE_DARK_MODE", "Enable Dark Mode", "0", Type.BOOLEAN, OptionPlatform.MAC, Visibility.VISIBLE },
084    };
085    
086    /**
087     * {@link Option}s can be either platform-specific or applicable to all
088     * platforms. Options that are platform-specific still appear in the 
089     * UI, but their component is not enabled.
090     */
091    public enum OptionPlatform { ALL, UNIXLIKE, WINDOWS, MAC };
092    
093    /**
094     * The different types of {@link Option}s.
095     * 
096     * @see TextOption
097     * @see BooleanOption
098     * @see MemoryOption
099     * @see DirectoryOption
100     * @see SliderOption
101     * @see LoggerLevelOption
102     * @see FileOption
103     */
104    public enum Type { TEXT, BOOLEAN, MEMORY, DIRTREE, SLIDER, LOGLEVEL, FILE };
105    
106    /** 
107     * Different ways that an {@link Option} might be displayed.
108     */
109    public enum Visibility { VISIBLE, HIDDEN };
110    
111    /** Maps an option ID to the corresponding object. */
112    private Map<String, ? extends Option> optionMap;
113    
114    private static OptionMaster instance;
115    
116    public OptionMaster() {
117        normalizeUserDirectory();
118        optionMap = buildOptions(blahblah);
119//        readStartup();
120    }
121    
122    public static OptionMaster getInstance() {
123        if (instance == null) {
124            instance = new OptionMaster();
125        }
126        return instance;
127    }
128    
129    /**
130     * Creates the specified options and returns a mapping of the option ID
131     * to the actual {@link Option} object.
132     * 
133     * @param options An array specifying the {@code Option}s to be built.
134     * 
135     * @return Mapping of ID to {@code Option}.
136     * 
137     * @throws AssertionError if the option array contained an entry that
138     * this method cannot build.
139     */
140    private Map<String, Option> buildOptions(final Object[][] options) {
141        // TODO(jon): seriously, get that zip stuff working! this array 
142        // stuff is BAD.
143        Map<String, Option> optMap = new HashMap<>(options.length);
144        
145        for (Object[] arrayOption : options) {
146            String id = (String)arrayOption[0];
147            String label = (String)arrayOption[1];
148            String defaultValue = (String)arrayOption[2];
149            Type type = (Type)arrayOption[3];
150            OptionPlatform platform = (OptionPlatform)arrayOption[4];
151            Visibility visibility = (Visibility)arrayOption[5];
152            
153            switch (type) {
154                case TEXT:
155                    optMap.put(id, new TextOption(id, label, defaultValue, platform, visibility));
156                    break;
157                case BOOLEAN:
158                    optMap.put(id, new BooleanOption(id, label, defaultValue, platform, visibility));
159                    break;
160                case MEMORY:
161                    optMap.put(id, new MemoryOption(id, label, defaultValue, platform, visibility));
162                    break;
163                case DIRTREE:
164                    optMap.put(id, new DirectoryOption(id, label, defaultValue, platform, visibility));
165                    break;
166                case SLIDER:
167                    optMap.put(id, new SliderOption(id, label, defaultValue, platform, visibility));
168                    break;
169                case LOGLEVEL:
170                    optMap.put(id, new LoggerLevelOption(id, label, defaultValue, platform, visibility));
171                    break;
172                case FILE:
173                    optMap.put(id, new FileOption(id, label, defaultValue, platform, visibility));
174                    break;
175                default:
176                     throw new AssertionError(type + 
177                         " is not known to OptionMaster.buildOptions()");
178            }
179        }
180        return optMap;
181    }
182    
183    /**
184     * Converts a {@link Platform} to its corresponding 
185     * {@link OptionPlatform} type.
186     * 
187     * @return The current platform as a {@code OptionPlatform} type.
188     * 
189     * @throws AssertionError if {@link StartupManager#getPlatform()} 
190     * returned something that this method cannot convert.
191     */
192    // a lame-o hack :(
193    protected OptionPlatform convertToOptionPlatform() {
194        Platform platform = StartupManager.getInstance().getPlatform();
195        switch (platform) {
196            case WINDOWS: 
197                return OptionPlatform.WINDOWS;
198            case MAC:
199                return OptionPlatform.MAC;
200            case UNIXLIKE: 
201                return OptionPlatform.UNIXLIKE;
202            default: 
203                throw new AssertionError("Unknown platform: " + platform);
204        }
205    }
206    
207    /**
208     * Returns the {@link Option} mapped to {@code id}.
209     * 
210     * @param id The ID whose associated {@code Option} is to be returned.
211     * 
212     * @return Either the {@code Option} associated with {@code id}, or 
213     * {@code null} if there was no association.
214     * 
215     * 
216     * @see #getMemoryOption
217     * @see #getBooleanOption
218     * @see #getDirectoryOption
219     * @see #getSliderOption
220     * @see #getTextOption
221     * @see #getLoggerLevelOption
222     * @see #getFileOption
223     */
224    private Option getOption(final String id) {
225        return optionMap.get(id);
226    }
227    
228    /**
229     * Searches {@link #optionMap} for the {@link MemoryOption} that 
230     * corresponds with the given {@code id}.
231     * 
232     * @param id Identifier for the desired {@code MemoryOption}. 
233     * Should not be {@code null}.
234     * 
235     * @return Either the {@code MemoryOption} that corresponds to {@code id} 
236     * or {@code null}.
237     */
238    public MemoryOption getMemoryOption(final String id) {
239        return (MemoryOption)optionMap.get(id);
240    }
241    
242    /**
243     * Searches {@link #optionMap} for the {@link BooleanOption} that 
244     * corresponds with the given {@code id}.
245     * 
246     * @param id Identifier for the desired {@code BooleanOption}. 
247     * Should not be {@code null}.
248     * 
249     * @return Either the {@code BooleanOption} that corresponds to {@code id} 
250     * or {@code null}.
251     */
252    public BooleanOption getBooleanOption(final String id) {
253        return (BooleanOption)optionMap.get(id);
254    }
255    
256    /**
257     * Searches {@link #optionMap} for the {@link DirectoryOption} that 
258     * corresponds with the given {@code id}.
259     * 
260     * @param id Identifier for the desired {@code DirectoryOption}. 
261     * Should not be {@code null}.
262     * 
263     * @return Either the {@code DirectoryOption} that corresponds to 
264     * {@code id} or {@code null}.
265     */
266    public DirectoryOption getDirectoryOption(final String id) {
267        return (DirectoryOption)optionMap.get(id);
268    }
269    
270    /**
271     * Searches {@link #optionMap} for the {@link SliderOption} that 
272     * corresponds with the given {@code id}.
273     * 
274     * @param id Identifier for the desired {@code SliderOption}. 
275     * Should not be {@code null}.
276     * 
277     * @return Either the {@code SliderOption} that corresponds to {@code id} 
278     * or {@code null}.
279     */
280    public SliderOption getSliderOption(final String id) {
281        return (SliderOption)optionMap.get(id);
282    }
283    
284    /**
285     * Searches {@link #optionMap} for the {@link TextOption} that 
286     * corresponds with the given {@code id}.
287     * 
288     * @param id Identifier for the desired {@code TextOption}. 
289     * Should not be {@code null}.
290     * 
291     * @return Either the {@code TextOption} that corresponds to {@code id} 
292     * or {@code null}.
293     */
294    public TextOption getTextOption(final String id) {
295        return (TextOption)optionMap.get(id);
296    }
297    
298    /**
299     * Searches {@link #optionMap} for the {@link LoggerLevelOption} that 
300     * corresponds with the given {@code id}.
301     * 
302     * @param id Identifier for the desired {@code LoggerLevelOption}. 
303     * Should not be {@code null}.
304     * 
305     * @return Either the {@code LoggerLevelOption} that corresponds to {@code id} 
306     * or {@code null}.
307     */
308    public LoggerLevelOption getLoggerLevelOption(final String id) {
309        return (LoggerLevelOption)optionMap.get(id);
310    }
311
312    public FileOption getFileOption(final String id) {
313        return (FileOption)optionMap.get(id);
314    }
315
316    // TODO(jon): getAllOptions and optionsBy* really need some work.
317    // I want to eventually do something like:
318    // Collection<Option> = getOpts().byPlatform(WINDOWS, ALL).byType(BOOLEAN).byVis(HIDDEN)
319    /**
320     * Returns all the available startup manager options.
321     * 
322     * @return Either all available startup manager options or an empty 
323     * {@link Collection}.
324     */
325    public Collection<Option> getAllOptions() {
326        return Collections.unmodifiableCollection(optionMap.values());
327    }
328    
329    /**
330     * Returns the {@link Option Options} applicable to the given 
331     * {@link OptionPlatform OptionPlatforms}.
332     * 
333     * @param platforms Desired platforms. Cannot be {@code null}.
334     * 
335     * @return Either a {@link List} of {code Option}-s applicable to
336     * {@code platforms} or an empty {@code List}.
337     */
338    public List<Option> optionsByPlatform(
339        final Collection<OptionPlatform> platforms) 
340    {
341        if (platforms == null) {
342            throw new NullPointerException("must specify platforms");
343        }
344        Collection<Option> allOptions = getAllOptions();
345        List<Option> filteredOptions = 
346            new ArrayList<>(allOptions.size());
347
348        filteredOptions.addAll(
349            allOptions.stream()
350                      .filter(option ->
351                              platforms.contains(option.getOptionPlatform()))
352                      .collect(Collectors.toList()));
353
354        return filteredOptions;
355    }
356    
357    /**
358     * Returns the {@link Option Options} that match the given 
359     * {@link Type Types}. 
360     * 
361     * @param types Desired {@code Option} types. Cannot be {@code null}.
362     * 
363     * @return Either the {@code List} of {@code Option}-s that match the given 
364     * types or an empty {@code List}.
365     */
366    public List<Option> optionsByType(final Collection<Type> types) {
367        if (types == null) {
368            throw new NullPointerException("must specify types");
369        }
370        Collection<Option> allOptions = getAllOptions();
371        List<Option> filteredOptions = 
372            new ArrayList<>(allOptions.size());
373        filteredOptions.addAll(
374            allOptions.stream()
375                      .filter(option -> types.contains(option.getOptionType()))
376                      .collect(Collectors.toList()));
377        return filteredOptions;
378    }
379    
380    /**
381     * Returns the {@link Option Options} that match the given levels of 
382     * {@link Visibility visibility}.
383     * 
384     * @param visibilities Desired visibility levels. Cannot be {@code null}.
385     * 
386     * @return Either the {@code List} of {@code Option}-s that match the given 
387     * visibility levels or an empty {@code List}. 
388     */
389    public List<Option> optionsByVisibility(
390        final Collection<Visibility> visibilities) 
391    {
392        if (visibilities == null) {
393            throw new NullPointerException("must specify visibilities");
394        }
395        Collection<Option> allOptions = getAllOptions();
396        List<Option> filteredOptions = 
397            new ArrayList<>(allOptions.size());
398        filteredOptions.addAll(
399            allOptions.stream()
400                      .filter(option ->
401                              visibilities.contains(option.getOptionVisibility()))
402                      .collect(Collectors.toList()));
403        return filteredOptions;
404    }
405    
406    public void normalizeUserDirectory() {
407        StartupManager startup = StartupManager.getInstance();
408        Platform platform = startup.getPlatform();
409        File dir = new File(platform.getUserDirectory());
410        File prefs = new File(platform.getUserPrefs());
411        
412        if (!dir.exists()) {
413            dir.mkdir();
414        }
415        if (!prefs.exists()) {
416            try {
417                File defaultPrefs = new File(platform.getDefaultPrefs());
418                startup.copy(defaultPrefs, prefs);
419            } catch (IOException e) {
420                System.err.println("Non-fatal error copying user preference template: "+e.getMessage());
421            }
422        }
423    }
424    
425    public void readStartup() {
426        File script =
427            new File(StartupManager.getInstance().getPlatform().getUserPrefs());
428        try (BufferedReader br = new BufferedReader(new FileReader(script))) {
429            String line;
430            while ((line = br.readLine()) != null) {
431                if (line.startsWith("#")) {
432                    continue;
433                }
434                int splitAt = line.indexOf('=');
435                if (splitAt >= 0) {
436                    String id = line.substring(0, splitAt).replace(SET_PREFIX, EMPTY_STRING);
437                    Option option = getOption(id);
438                    if (option != null) {
439                        System.err.println("setting '"+id+"' with '"+line+'\'');
440                        option.fromPrefsFormat(line);
441                    } else {
442                        System.err.println("Warning: Unknown ID '"+id+'\'');
443                    }
444                } else {
445                    System.err.println("Warning: Bad line format '"+line+'\'');
446                }
447            }
448        } catch (IOException e) {
449            System.err.println("Non-fatal error reading the user preferences: "+e.getMessage());
450        }
451    }
452    
453    public void writeStartup() {
454        File script = 
455            new File(StartupManager.getInstance().getPlatform().getUserPrefs());
456        if (script.getPath().isEmpty()) {
457            return;
458        }
459        // TODO(jon): use filters when you've made 'em less stupid
460        String newLine = 
461                StartupManager.getInstance().getPlatform().getNewLine();
462        OptionPlatform currentPlatform = convertToOptionPlatform();
463        StringBuilder contents = new StringBuilder(2048);
464        for (Object[] arrayOption : blahblah) {
465            Option option = getOption((String)arrayOption[0]);
466            OptionPlatform platform = option.getOptionPlatform();
467            if ((platform == OptionPlatform.ALL) || (platform == currentPlatform)) {
468                contents.append(option.toPrefsFormat()).append(newLine);
469            }
470        }
471        
472        try (BufferedWriter out = new BufferedWriter(new FileWriter(script))) {
473            out.write(contents.toString());
474        } catch (IOException e) {
475            logger.error("Could not write to McIDAS-V startup prefs file", e);
476        }
477    }
478}